面向对象编程

C#是一种现代化的编程语言,也是一种面向对象编程(OOP)语言,它以操作对象为中心,将数据与其操作相结合,使得代码更加耦合。C#中的类有着更多的成员类型,可以更加灵活的组织代码并实现各种功能,接下来,我们从面向对象中的两个基本概念开始了解C#编程语言。

类与对象

类(class)和对象(object)是面向对象编程中最基本的两个概念。类定义了一个基本架构,包括属性、方法等成员,其中,属性表示数据,方法表示操作。对象则是类的具体表现。

下面,在VS中的“解决方案资源管理器”中,可以通过项目的右键菜单“添加”>>“添加新项”,也可以直接通过VS的菜单项“项目”>>“添加新项”。在“添加新项”窗口中,选择“VisualC#”>>“类”,并修改名称为CAuto.cs,如下图所示。

然后,我们修改CAuto.cs文件的内容如下。

C#
using System;
public class CAuto
{
    public String Model { get; set; }
    public void Drive()
    {
        System.Console.WriteLine(Model + " 行驶中...");
    }
}

代码中,CAuto就是一个类,使用class关键字及相关修饰符定义。CAuto类中,Model定义为一个属性(property),表示汽车型号,这是一个文本数据(字符串,String),使用一对双引号定义;Drive()是一个方法,其中执行的操作就是在控制台显示一条信息,当然,这种操作只限于命令行应用程序;此外,这里使用+运算符将两段文本连接起来。

下面的代码,我们在Main()方法中测试CAuto类的使用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CAuto car = new CAuto();
            car.Model = "C19";
            car.Drive();
        }
    }
}

通过键盘Ctrl+F5功能键执行程序,代码显示结果如下图。

这里,car定义为一个对象,它属于CAuto类,称为CAuto类的一个实例,需要使用new关键字进行实例化操作,而CAuto()称为构造方法,用于执行对象的初始化操作。创建car对象后,设置它的Model属性为C19,并调用Drive()方法执行操作。

需要判断一个对象是否为某个类的实例时,可以使用is运算符,如下面的代码。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CAuto car = new CAuto();
            Console.WriteLine(car is CAuto);
            Console.WriteLine(car is String);
        }
    }
}

代码执行结果如下图。

再次回顾类与对象的概念。类定义了一个结构框架,定义了属性、方法等成员,对象是类的实例,需要使用new关键字和构造方法进行初始化;如果对象没有实例化,则为空引用,使用null关键字表示。使用对象调用的成员称为实例成员,如示例中的Model属性和Drive()方法。

类成员概述

在类中可以定义的成员类型包括:

  • 构造方法,也称为构造函数,用于对象的初始化。
  • 析构方法,也称为析构函数,在对象清理时执行,如资源的释放、清理工作。
  • 常量,使用const关键字定义的,数据不会改变的标识。
  • 字段,使用变量(对象)定义的数据项。
  • 属性,数据项。
  • 方法,执行操作的主要类型。
  • 索引器,使用索引值访问成员的方式,如arr[0]、dict["mars"]。
  • 运算符,可定义运算符的运算规则。
  • 事件,定义一个代码的接入点,可以在指定的时机触发,执行代码可由对象的使用者编写。
  • 委托,实现事件的原始类型。
  • 类,类的内部可以定义嵌入类。
  • 接口,定义抽象结构的类型。
  • 结构,与类类型相似,但结构是值类型,而类是引用类型。

接下来的课程,我们会逐步了解这些成员的应用。

数据类型概述

软件开发中,数据的处理是最基本的操作,如数值处理、文本处理、日期和时间处理等等。与C、C++、Java等编程语言不同,C#中没有原生数据类型。C#中的数据类型都是.NET Framework数据类型的映射,而.NET平台中的数据类型包括引用类型和值类型,主要的类型包括:

  • 引用类型,包括类、委托。
  • 值类型,包括结构、枚举。

C#中的基本数据类型有byte、sbyte、short、ushort、int、uint、long、ulong、float、double、decimal、char、string。这些类型与.NET Framework类型的对照,以及数据的处理范围可以参考.NET Framework基本数据类型

这里,先初步了解两个基本的数据类型。int类型,处理32位整数(不包含小数部分)。string类型,处理文本内容,使用一对双引号定义字面量,如"abc"。下面的示例,我们会先主要使用这两种数据类型。

字段与属性

字段(field)和属性(property)都可以定义类中的数据,先来看字段的应用。下面的代码,我们在CAuto类中添加一个DoorCount字段,用于表示车门数量。

C#
using System;
public class CAuto
{
    public int DoorCount;
    public String Model { get; set; }
    public void Drive()
    {
        System.Console.WriteLine(Model + " 行驶中...");
    }
}

CAuto类中使用int类型的变量DoorCount表示车门数量,我们可以在Main()方法中测试它的使用,如下面的代码。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CAuto car = new CAuto();
            car.DoorCount = -1;
            Console.WriteLine(car.DoorCount);
        }
    }
}

执行代码会显示-1,这显示有些不科学,那么,如何控制数据的设置工作呢?一方面,可以在car对象使用前对数据量判断,另一方面,也可以将DoorCount变成属性,并在设置数据时进行检查;显然,后者可以将判断工作放在CAuto类中,对代码进行有效的封装,如下面的代码。

C#
using System;
public class CAuto
{
    private int myDoorCount;
    public int DoorCount
    {
        get
        {
            return myDoorCount;
        }
        set
        {
            if (value >= 0 && value <= 5)
                myDoorCount = value;
            else
                myDoorCount = 4;
        }
    }
    public String Model { get; set; }
    public void Drive()
    {
        System.Console.WriteLine(Model + " 行驶中...");
    }
}

代码中,使用凡DoorCount字段保存实际的车门数量,并定义为私有的(private),这样,它就只能有CAuto类的内部访问;然后定义了DoorCount属性,它包含两个部分,即get语句块中使用return语句返回数据,这里就是返回myDoorCount字段的数据;set语句块用于设置数据,其中,value表示设置属性带入的数据,当它大于等于0并小于等于5时,将数据赋值到myDoorCount字段,否则将myDoorCount的值设置为4。

下面的代码演示了DoorCount属性的应用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CAuto car = new CAuto();
            car.DoorCount = -1;
            Console.WriteLine(car.DoorCount);
            car.DoorCount = 5;
            Console.WriteLine(car.DoorCount);
        }
    }
}

代码执行结果如下图。

一般来讲,属性使用get语句块读取数据,使用set语句块设置数据;如果属性的数据不需要特殊操作,只是普通的读取和设置,可以简化get和set语句的代码,如Model属性的定义就可以写成“public Model { get; set; }”。

如果属性值只能写入不能读取数据,可以只包含set语句块,但这种情况并不多见。当属性值不能写入,只能读取时称为只读属性,只读属性的是指不能在类的外部,如使用类的实例设置属性值,但它还是可以在特定的地方设置数据,如通过构造方法,稍后会讨论相关内容。

方法

方法用于执行特定的功能,CAuto类中的Drive()方法用于显示行驶信息,下面的代码,我们在CAuto类中添加一个MoveTo()方法。

C#
using System;
public class CAuto
{
    public void MoveTo(int x, int y)
    {
        Console.WriteLine("{0}移动到({1},{2})", Model, x, y);
    }
    // 其它代码
}

下图显示了MoveTo()方法的各个部分。

其中:

  • 修饰符定义了方法的访问级别等信息。
  • 返回值类型定义方法返回数据的类型,void表示方法不需要返回数据。
  • 方法名,即方法的名称。
  • 参数列表,定义需要带入方法的数据,可以没有,也可以有一个或多个。
  • 方法体是完成方法功能的代码块,如果方法需要返回数据,需要使用return语句完成。

下面的代码,我们在Main()方法中测试MoveTo()方法的应用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CAuto car = new CAuto();
            car.Model = "X19";
            car.MoveTo(66, 1);
        }
    }
}

代码执行结果如下图。

下面了解几种参数应用的特殊情况,主要包括:

  • 按值传递。
  • 按引用传递。
  • 参数数组。
  • 参数默认值。
  • 输出参数。

默认情况下,如果参数是值类型,会按值传递到方法中,相应的,如果参数是引用类型,则会按引用传递到方法。下面的代码显示了按值传递的效果。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            int num = 10;
            Console.WriteLine(num);
            Test(num);
            Console.WriteLine(num);
        }
        static void Test(int x)
        {
            x = 99;
        }
    }
}

代码执行结果如下图。

本例,Test()方法的参数定义为int类型,这是一个值类型,在传递时会复制数据,所以,在Test()方法中修改参数的值只是修改了数据副本的数据,而Main()方法中的x的数据并没有被修改。

如果需要在方法修改值类型参数的数据,可以在参数将使用ref关键字,如下面的代码。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            int num = 10;
            Console.WriteLine(num);
            Test(ref num);
            Console.WriteLine(num);
        }
        static void Test(ref int x)
        {
            x = 99;
        }
    }
}

代码执行结果如下图。

实际应用中,不只是方法参数的传递,在赋值操作时,同样存在值传递和引用传递的情况,如下面的代码,我们使用两个CAuto类的实例操作显示引用类型的赋值操作。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CAuto car1 = new CAuto();
            CAuto car2 = car1;
            car1.Model = "C10";
            Console.WriteLine(car1.Model);
            Console.WriteLine(car2.Model);
            car2.Model = "C99";
            Console.WriteLine(car1.Model);
            Console.WriteLine(car2.Model);
        }
    }
}

代码执行结果如下图。

本例,由于类是引用类型,所以,将car1对象赋值给car2对象时传递引用,也就是说,此时,car1和car2对象实际指向的是同一对象体;无论是通过car1对象修改数据,还是通过car2对象修改数据,其变化都可以通过两个对象反映出来。

参数数组可以定义一个相同类型的参数序列,使用方法时可以不指定参数,也可以指定一个或多个参数,如下面的代码。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Add(1, 2));
            Console.WriteLine(Add(1, 2, 3));
            Console.WriteLine(Add(1, 2, 3, 4));
        }
        static int Add(int x, int y, params int[] nums)
        {
            int sum = x + y;
            for (int i = 0; i < nums.Length; i++)
            {
                sum += nums[i];
            }
            return sum;
        }
    }
}

代码执行结果如下图。

本例中定义了Add()方法,用于将计算两个或更多int数据的和。方法中,第三个参数使用params关键字定义了参数数组,其成员类型为int。Main()方法中,分别使用了两个参数、三个参数和四个参数。使用参数数组时应注意,应将它放在方法的参数序列的最后。

此外,在代码中使用了+=运算符,如“x += y”的含义就是“x = x + y”。

关于数组、for语句结构会在后续的课程中详细介绍。

在定义方法的参数时,还可以指定默认值,如果调用方法时不指定数据,在方法中就使用参数指定的默认值,如下面的代码。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(IntPwd(3));
            Console.WriteLine(IntPwd(3,4));
        }
        static int IntPwd(int x, int n = 2)
        {
            int result = x;
            for (int i = 1; i < n; i++)
            {
                result *= x;
            }
            return result;
        }
    }
}

代码执行结果如下图。

本例,IntPwd()方法用于计算参数x的n次方,默认是求2次方。Main()方法中的第一次调用,没有指定n参数,则默认就使用2;第二次调用指定n参数为4,则就x的4次方。

使用参数的默认值时应用注意,有默认值的参数应在所有没有默认值参数的后面;而且,有默认值的参数不应和参数数组一起使用。

除了使用返回值从方法中返回数据,还可以通过输出参数返回数据,如下面的代码。

C#
public static bool TryParse(string s,out int result)

这是int类型中定义的TryParse()方法,其功能是尝试将字符串内容转换为int类型,如果返回成功,方法返回true值,转换后的数据由result参数返回,请注意,result参数使用out关键字定义为输出参数。下面的代码演示了此方法的应用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            int x;
            Console.WriteLine(int.TryParse("abc", out x));
            Console.WriteLine(x);
            Console.WriteLine(int.TryParse("123", out x));
            Console.WriteLine(x);
        }
    }
}

代码执行结果如下图。

本例共有四个输出。前两个输出,由于字符串"abc"不能转换为整数,所以会显示False(假),输出的x的数据为整数的默认值0;后两个输出,由于"123"可以转换为整数,所以显示True(真),输出的x的数据为转换后的结果,即123。

方法重载

方法重载是指,存在多个同名的方法,但可以通过不同的参数定义来区分不同的方法版本。调用方法时,可以根据带入的参数自动执行最合适的版本。

实际上,前面介绍的参数数组和参数默认值都是创建多版本方法的方式,但它们也有一些特点,如参数数组中使用的是相同类型的数据,而使用参数的默认值时,参数也是相对固定的。如果方法的参数的含义和类型都不同,则需要使用方法重载。如下面的代码,我们定义了两个Add()方法。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Add(10, 99));
            Console.WriteLine(Add("10", "99"));
        }
        //
        static int Add(int x, int y)
        {
            return x + y;
        }
        //
        static string Add(string x, string y)
        {
            return x + y;
        }
    }
}

代码执行结果如下图。

本例,第一个输出指定了两个整数参数,其默认就是int类型,所以调用的是Add(int,int)方法,返回两个整数相加的结果。第二个输出指定了两个字符串参数,实际调用的是Add(string,string)方法,返回两个字符串相加的结果,也就是两个字符串连接到一起的结果。

构造方法

构造方法,又称为构造函数,用于对象的初始化工作。构造方法与方法的定义相似,但也有明显的不同,如构造方法应与类同名、构造方法不需要返回值等;此外,构造方法也可以有多个重载版本。下面的代码,我们在CAuto类中添加三个构造方法。

C#
using System;
public class CAuto
{
    // 构造方法
    public CAuto(string sModel ,int iDoorCount)
    {
        Model = sModel;
        DoorCount = iDoorCount;
    }
    public CAuto(string sModel) : this(sModel, 4) { }
    public CAuto() : this("", 4) { }
    // 只读属性
    public String Model { get; private set; }
    // 其它代码
}

代码中,首先定义了主构造方法,即CAuto(string,int),其参数分别用于指定车辆型号和车门数量;接下来的两个构造方法,通过:符号及this关键字调用了CAuto(string,int)函数,并设置了相应的默认值,即型号默认为空字符串(""),车门数量默认为4。

此外,请注意Model属性的定义,在set语句块前使用了private关键字;这样,在类的外部就不能给Model属性赋值,也就是说,Model属性定义为一个只读属性。一般来讲,只读属性的数据可以通过构造方法设置,如上述代码中的操作。

下面,我们来测试这几个构造方法的应用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CAuto car = new CAuto("C19");
            Console.WriteLine(car.Model);
            Console.WriteLine(car.DoorCount);
            CAuto suv = new CAuto("X20", 5);
            Console.WriteLine(suv.Model);
            Console.WriteLine(suv.DoorCount);
        }
    }
}

代码执行结果如下图。

关于构造方法,我们在“继承”相关内容中还会有进一步的讨论。

析构方法

C#应用中,大多数情况下,资源都可以自动管理,并在不需要的时候自动清理;特殊情况下,可能在释放对象时需要进一步清理外部资源,可以通过析构方法完成。

析构方法,又称为析构函数,会在对象清理时自动调用。在类中定义析构方法,需要在构造方法名前添加~符号,而且,析构方法不需要任何修饰符,如下面的代码。

C#
using System;
public class CAuto
{
    // 析构方法
    ~CAuto()
    {
        Console.WriteLine(Model + "车辆已报废");
    }
    // 其它代码
}

下面,在Main()方法中测试析构方法的应用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CAuto car = new CAuto("C19");
            CAuto suv = new CAuto("X20", 5);
        }
    }
}

代码执行结果如下图。

本例中只包含了对象的创建代码,并没有对象的销毁代码,但我们可以看到,CAuto中的析构方法已自动调用。

在介绍try语句结构、using语句结构和IDisposable接口时还会有关于自动清理的相关内容。

静态类与静态成员

静态类的主要用途就是代码的封装,如Math类中就封装了大量的数学计算资源;在类中使用static关键字定义的成员称为静态成员,如下面的代码,我们在CAuto类中添加一个只读属性ObjCounter,用于统计创建了多少对象;请注意,ObjCounter属性定义为静态属性。

C#
using System;
public class CAuto
{
    // 对象计数器
    public static int ObjCounter { get; private set; }
    // 静态构造方法
    static CAuto()
    {
        ObjCounter = 0;
    }
    // 构造方法
    public CAuto(string sModel ,int iDoorCount)
    {
        Model = sModel;
        DoorCount = iDoorCount;
        ObjCounter++;
    }
    // 其它代码
}

除了静态的只读属性,我们还定义了静态构造方法,其中将ObjCounter属性初始值设置为0。下面的代码,我们来测试静态成员的使用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            CAuto car = new CAuto("C19");
            CAuto suv = new CAuto("X20", 5);
            Console.WriteLine(CAuto.ObjCounter);
            CAuto couper = new CAuto("R19", 2);
            Console.WriteLine(CAuto.ObjCounter);
        }
    }
}

代码执行结果如下图。

与实例成员不同,静态成员可以直接使用类来调用,所以,静态成员也称为类成员。此外,静态成员也可以在实例成员中调用。

扩展方法

静态类和静态方法的另一个重要用途是可以扩展已有类型的操作方法,如下面的代码,我们创建一个静态类CNum,其中扩展int类型的方法,用于判断一个整数是否为偶数。

C#
using System;
public static class CNum
{
    public static bool IsEven(this int num)
    {
        return num % 2 == 0;
    }
}

代码中,在CNum静态类中定义了一个IsEven ()静态方法,请注意,方法的第一个参数使用了this关键字,其功能就是定义了扩展的是哪个类型的方法,而且参数带入到方法内的数据就是实际操作的数据。下面的代码,我们在Main()方法中测试此方法的应用。

C#
using System;
namespace HelloProject
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(2.IsEven());
            Console.WriteLine(3.IsEven());
        }
    }
}

代码执行结果如下图。

本站内容均为原创作品,转载请注明出处,本页面网址为:http://caohuayu.com/chy/article/Article.aspx?code=cc002002