第三章 对象和类型

类和结构

​ 类是引用类型, 实例化某个类的对象需要使用new运算符, 将对象的内存分配在托管堆上. 类按引用传递.

​ 结构不同于类, 结构不需要在堆上分配空间. 结构是值类型, 通常存储在栈上. 结构按传递. 另外,结构不支持继承.

  • new运算符并不像C++的new一样总是在堆上分配内存
    • 若new的对象是类, 则会在堆上分配内存并调用构造函数进行初始化
    • 若new的对象是结构, 则不会分配内存, 只会调用结构的构造函数对栈上的结构进行初始化
      • 结构对象分配内存发生在变量的声明处, 而类变量声明只会创建引用

​ 类包含成员, 成员可以是静态(static)成员或实例成员. 静态成员属于(belong to)类, 实例成员属于对象. 静态成员的值对每个对象都是相等的. ​ 成员的种类见下表.

成员 描述
字段 字段是在类范围声明的变量。 字段可以是内置数值类型或其他类的实例。 例如,日历类可能具有一个包含当前日期的字段。
常量 常量是在编译时设置其值并且不能更改其值的字段。
属性 属性是类中可以像类中的字段一样访问的方法。 属性可以为类字段提供保护,以避免字段在对象不知道的情况下被更改。
方法 方法定义类可以执行的操作。 方法可接受提供输入数据的参数,并可通过参数返回输出数据。 方法还可以不使用参数而直接返回值。
事件 事件向其他对象提供有关发生的事情(如单击按钮或成功完成某个方法)的通知。 事件是使用委托定义和触发的。
运算符 重载运算符被视为类型成员。 重载运算符时,将其定义为类型中的公共静态方法。 有关详细信息,请参阅运算符重载
索引器 使用索引器可以用类似于数组的方式为对象建立索引。
构造函数 构造函数是首次创建对象时调用的方法。 它们通常用于初始化对象的数据。
终结器(析构函数) C# 中很少使用终结器。 终结器是当对象即将从内存中移除时由运行时执行引擎调用的方法。 它们通常用来确保任何必须释放的资源都得到适当的处理。
嵌套类型 嵌套类型是在其他类型中声明的类型。 嵌套类型通常用于描述仅由包含它们的类型使用的对象。

字段

​ 与类相关的变量. 使用 const 关键字声明常量. 如果字段被声明为 public, 就可以在类的外部使用语法Object.FieldName在类的外部访问.

只读字段

​ 带有readonly修饰符的字段只能在构造函数中分配值. 与常量字段相反(常量字段总是static的), 只读字段可以是实例成员, 若要使用静态只读字段, 需要显式声明为 static.

​ 最好不把字段声明为 public, 而是声明为 private, 并使用属性访问字段.

属性

​ 属性(property)的概念是: 它是一个或一对方法, 在客户端代码(调用方)看来, 它是一个字段. ​ 属性可以包含 get 和 set 访问器, 分别用于访问和设置字段的值.

class PhoneCustumer
{
    private string _firstName;
    public string FirstName
    {
        get{ return _firstName; }
        set{ _firstName = value; }
    }
    //...
}

get访问器不带任何参数, 且必须返回属性声明的类型. ​ 不应为set访问器指定任何显式参数. set访问器带一个隐式参数value, 其类型与属性相同. ​ 上面的程序中, 字段 _firstName 作为属性 FirstName 的后备变量, 即实际存储属性数据的变量.

具有表达式体的属性访问器

​ 可以将上述属性改为以下写法:

private string _firstName;
public string FirstName
{
    get => _firstName;
    set => _firstName = value;
}

​ 这个特性减少了编写花括号的需求, 并省略了 get 访问器的return语句, 但使用此特性的访问器实现只能由一条语句构成.

自动实现的属性

​ 如果属性的 get 和 set 访问器中没有任何逻辑, (暂时)只用于设置值, 就可以使用自动实现的属性. 表示年龄的属性可以使用以下写法:

public int Age { get; set; } = 42;

​ 自动实现的属性可以使用属性初始化器来初始化. 属性初始化器实际会为类创建一个不含参数的默认构造函数, 将初始化器的设置默认值过程代码放在默认构造函数的函数体开头. ​ 不需要声明私有字段作为后备变量, 因为编译器会自动隐式创建它. 使用自动实现的属性就不能直接访问字段, 因为不知道编译器生成的名称. ​ 由于不能含有任意更多的语句, 上述代码无法验证属性中设置的值是否有效.

属性的访问修饰符

​ C#允许为属性的 set 与 get 访问器设置不同访问修饰符, 所以属性可以有公有的 get 访问器和受保护的 set 访问器. 访问器的缺省访问级别为属性的访问级别. 两个访问器至少有一个具有属性级别的访问级别, 否则会造成编译错误.

只读属性

​ 省略 set 访问器, 就可以创建只读属性. ​ 可以使用相同的方式创建只写属性, 但这不是好的编程方式, 因为这样使用属性会令人感到迷惑. 如果有这样的需求, 应当使用方法代替.

表达式体属性

​ 只有 get 访问器的属性可以使用表达式体属性实现.

public string FirstName { get; }
public string LashName { get; }
public string FullName => $"{FirstName} {LastName}";

​ 表达式体属性是只带有 get 访问器的属性, 但不需要编写 get 关键字, 只是属性的实现处使用 lambda 运算符=>构成表达式主体.

不可变类型

​ 如果对象没有任何可以改变的成员, 只有只读成员, 其内容只能在初始化时设置, 那么它的类型就是不可变类型.

  • 这里不提常量是因为常量总是静态的, 不会属于某个成员

​ 这种对象对于多线程是非常有用的, 因为多个线程可以同时访问信息不会改变的一个对象. 因为数据不会改变, 所以不需同步.

匿名类型

​ 匿名类型是一个继承自Object且没有名称的类. varnew关键字一起使用时, 可以创建匿名类型. 该类的对象类型由编译器经初始化器推断, 类似于隐式类型化的变量.

var captain = new
{
    FirstName = "James",
    LastName = "Kirk"
};

​ 这会生成一个包含如上两个属性的对象. 如果创建如下另一个对象:

var doctor = new
{
    FirstName = "Leonard",
    LastName = "McCoy"
}

那么 captain 与 doctor 的类型相同. 例如, 表达式captain = doctor;是合法的. 只有所有属性都匹配, 二者类型才会相同. 如果所设置的值来自另一个对象, 则可以推断匿名类型的成员.

var person = new
{
    doctor.FirstName,
    doctor.LastName
};

这个新对象的属性与 doctor 相同.

​ 这些新对象的类型名未知. 编译器会为这些匿名类型生成一个名称, 但只有编译器才能使用它. 我们不能也不应使用匿名类型对象上的任何类型反射, 因为这不会得到一致的结果.

方法

​ 注意, 正式的C#术语区分函数与方法. 在C#术语中, “函数成员“不仅包括方法, 也包括类或结构的一些非数据成员, 如索引器, 运算符, 构造函数, 析构函数, 属性等. 这些都不是数据成员, 字段, 常量, 事件才是数据成员.

方法的声明

​ 方法定义包含: 修饰符, 返回值类型, 方法名, 参数列表, 方法体.

[modifiers] return_type MethodName ([parameters])
{
    // Method body
}

表达式体方法

​ 用于实现只有一条语句的方法. 不需要编写花括号与 return 语句, 而使用运算符=>区分操作符左边的声明与右边的实现代码.

public bool IsSquare(Rectangle rect) => rect.Height == rect.Width;

这段代码等价于

public bool IsSquare(Rectangle rect)
{
    return rect.Height == rect.Width;
}

=>运算符右边的表达式的值是方法的返回值, 因此表达式的值必须与方法返回值类型相同.

方法重载

​ C#支持方法重载, 即允许多个同名但不同签名方法的存在.

  • 方法签名包括方法返回值类型, 参数列表等, 参数列表中包含参数个数与每个参数类型.
    • 但不允许只有返回值不同的多个方法同时存在, 因为这会导致编译器无法区分该调用哪一个方法

显式指定名称的参数

​ 考虑如下方法签名:

public void MoveAndResize(int x, int y, int width, int height)

​ 用下面代码片段调用它, 无法从中看出使用了什么数字, 与这些数字用于哪里:

r.MoveAndResize(30, 40, 20, 40);

​ 可以显示指定参数在签名中的名称, 明确这些数字的含义:

r.MoveAndResize(x: 30, y: 40, width: 20, height: 40);

​ 对于这样调用的方法, 编译器会去掉变量名, 创建一个方法调用; 这在编译后的代码中没有差别. ​ 还可以用这种方法更改变量的顺序, 编译器会重新安排, 获得正确的顺序. 其真正优势在于可选参数的方法.

可选参数

​ 可以为参数提供默认值使参数变为可选参数. 可选参数必须位于方法参数列表的最后.

public void Test(int n, int opt1 = 1, int opt2 = 2, int opt3 = 3)

​ 使用命名参数,可以传递任何位置的参数, 例如, 下面的例子仅传递最后一个可选参数:

Test(0, opt3: 9);

个数可变的参数

​ 声明数组类型的参数, 添加params关键字, 就可以使用任意数量的同类型参数调用该方法. 如果 params 关键字与方法签名定义的多个参数一起使用, 则 params 只能使用一次, 并且它必须是最后一个参数.

Console.WriteLine(string format, params object[] arg)

​ 由于 object 是所有类型的父类型, 所以使用 object 数组可以把不同类型参数传递给方法.

构造函数

  • 与C++区别: 单独包含修饰符(public等), 且不可使用初始化列表

​ 如果没有提供任何构造函数, 编译器会在后台生成一个无参数的默认构造函数, 用来把所有字段初始化为标准的默认值(数值类型为0, 引用类型为 null, bool 类型为 false). ​ 构造函数重载与其他方法规则相同. ​ 可以将构造函数限定为privateprotected, 这样不相关的类就无法访问它们.

public class MyNumber
{
    private int _number;
    private MyNumber(int number)
    {
        _number = number;
    }
}

​ 这个类没有提供任何公有的或受保护的构造函数, 这使得MyNumber类不能使用new运算符在外部代码中实例化(但可以在MyNumber)中编写一个公有静态方法或属性以实例化此类. 在下面两种情况是有用的:

  • 类仅用作静态成员或属性的容器, 因此永远不会实例化它. 这种情况下, 可以使用static修饰该类, 这会使得类只能包含静态成员, 不能实例化.

  • 希望类仅通过调用某个静态成员函数来实例化(即所谓对象实例化的类工厂方法). 单例模式的实现如下:

    public class Singleton
    {
        private static Singleton s_instance;
        private int _state;
        private Singleton(int state)
        {
            _state = state;
        }
        public static Singleton Instance
        {
            get => s_instance ?? (s_instance = new Singleton(42));
        }
    }
    

    Singleton类构造函数为私有, 所以只能在类内实例化它本身. 静态属性Instance返回字段s_instance, 如果这个字段尚未初始化(null), 则计算并返回??运算符右侧表达式的值, 调用构造函数创建一个实例. 若字段已经初始化, 则返回字段本身.

表达式体和构造函数

​ 若构造函数只有一个语句, 则也可以使用表达式体语句.

public class Singleton
{
    private static Singleton s_instance;
    private int _state;
    private Singleton(int state) => _state = state;
    public static Singleton Instance => s_instance ?? (s_instance = new Singleton(42));
}

构造函数初始化器

​ 该特性可以解决多个构造函数中有大量重复代码的问题.

class Car
{
    private string _description;
    private uint _nWheels;
    public Car(string description, uint nWheels)
    {
        _description = description;
        _nWheels = nWheels;
    }
    public Car(string description): this(description, 4) {}
}

​ 这里, this关键字仅调用参数最匹配的构造函数. 编译器会先调用this匹配的构造函数, 再执行当前构造函数的函数体.

  • 有时也可以用参数默认值实现相同的功能

​ 构造函数初始化器可以包含对同一个类另一个构造函数的调用, 也可以包含对直接基类构造函数的调用(将this关键字改为base). ​ 初始化器中不能有多个调用.

静态构造函数

​ 静态构造函数的声明方法是

static classname()
{
    // initialization code
}

​ 静态构造函数负责初始化类的静态字段和属性. ​ 静态构造函数只能访问类的静态成员, 不能访问实例成员. ​ .NET 运行库不确保什么时候执行静态构造函数, 也不保证多个类静态构造函数的执行顺序, 所以其中不应包含要求在某个时刻(如加载程序集时)执行的代码. .NET 运行库保证静态构造函数在第一次调用类的任何成员前执行且仅执行一次. ​ 静态构造函数没有访问修饰符, 因为它从不被显式调用, 而是由.NET 运行库在加载类前调用. 出于同样原因, 静态构造函数不能有任何参数, 并且一个类最多有一个静态构造函数(不允许重载).

结构

​ 类将数据存储在托管堆上, 这种存储方式可以在数据的生存期上获得很大灵活性, 但会有一定的性能损失(得益于 C# 托管堆的优化, 这种性能损失较小). ​ 结构是类型, 使用结构将会使数据存储在栈上或存储为内联(如果它们是存储在堆中对象的一部分), 其生存期限制与简单数据类型相同. 当仅需要较小的数据结构时, 结构可以带来更优秀的性能.

  • 结构不支持继承
  • 结构的构造函数工作方式与类有些不同
    • 编译器总是隐式为结构提供无参数的构造函数(默认构造函数), 即使定义了有参数的构造函数
    • 默认构造函数总是把数值字段都初始化为0, 不能为结构创建定制的默认构造函数, 只能定制有参数的构造函数
  • 使用结构可以指定字段如何在内存中布局(16章介绍特性时将详细讨论)

​ 一般地, 结构只是数据项的简单集合, 所以所有或大多数字段都声明为 public.

在后台上, int 类型(System.Int32)是一个具有公共字段的结构. 新类型System.ValueType是一个包含一个或多个公共字段的结构.

结构是值类型

Dimensions point;
point.Length = 3;
point.Width = 6;

​ 如果 Dimensions 是一个类, 就会产生一个编译错误, 因为 point 包含一个未初始化的引用, 所以不能给其字段设置值. 但对于结构, 变量声明实际上是为整个结构在栈中分配空间, 所以语句Dimensions point;已经为该结构对象分配了空间, 自然就可以复制了. ​ 但注意下面的代码会产生编译错误, 编译器会指出用户使用了未初始化的变量:

Dimensions point;
double d = point.Length;

这是因为语句Dimensions point;只是为 point 分配了内存, 并未初始化. 考虑以下代码:

var point = new Dimensions();
point.Length = 3;
point.Width = 6;

结构在语法上常常可以当作类来处理. 但结构的new运算符与类和其他类型工作方式不同. 结构的new运算符不分配堆中的内存, 而只是根据传递的参数调用相应的构造函数, 初始化所有字段.

​ 由于结构内联或保存在栈中, 分配内存速度非常快; 栈上的结构超出作用域被删除时, 不需要等待垃圾回收, 速度也很快. 但在某些条件下结构也会对性能造成负面影响. 如果将结构作为参数来传递或把一个结构赋值给另一个结构, 结构的所有数据都将被复制, 而对于类, 只复制引用. 这样根据结构大小, 会有相应的性能损失. ​ 当把结构作为参数传递给方法时, 应作为ref参数传递.

只读结构

readonly修饰符可以用于结构, 以保证结构体的不变性.

结构和继承

​ 结构不是为继承设计的(结构只适合小的数据结构), 这意味着它不能从一个结构中继承.

​ 结构与其他类型一样派生自类System.Object, 在结构中也可以重写System.Object中的方法. ​ 结构的继承链: 每个结构派生自System.ValueType类, System.ValueType类又派生自System.Object. ​ ValueType并没有给 Object 添加任何新成员, 但提供了一些更合适的结构实现方法. ​ 注意, 不能为结构提供其他基类: 每个结构都派生自ValueType.

只有结构作为对象时才从ValueType中继承. 不能用作对象的结构是引用结构. 参见稍后的”ref 结构”.

​ 要比较结构值, 最好实现接口IEquatable<T>.

ref 结构

​ 结构并不总是放在栈上, 也可以放在堆上. 可以将ref修饰符应用于结构而创建的.

对于大多数程序, 不需要创建自定义ref struct类型, 但对于需要减少垃圾收集的高性能应用程序, 需要使用这种类型. 更多信息请参考17章, 其中详细介绍了Span类型以及关于ref的更多信息.

按值传递和按引用传递参数

​ 基本规则同 C++.

ref 参数

​ 通过在参数列表添加ref修饰符可以实现按引用传递值类型对象. ​ 考虑下列代码, 其中 A 是结构类型:

public static void ChangeA(ref A a)
{
    a.X = 2;
}
static void Main()
{
    A a1 = new A { X = 1; };
    ChangeA(ref a1);
    Console.WriteLine($"{a1.X}");	//output: 2
}

需要注意的是ref关键字在调用方法时也需要添加在参数前.

​ 现在考虑类类型在参数有无ref时的行为.

​ 下面的代码中, A 为类:

public static void ChangeA(A a)
{
    a.X = 2;
    a = new A { X = 3; }
}
static void Main()
{
    A a1 = new A { X = 1; };
    ChangeA(a1);
    Console.WriteLine($"{a1.X}");	//output: 2
}

类本身按照引用传递, 所以语句a.X = 2;将 Main 中 a1 的字段 X 设置为2; 然而下一行a = new A { X = 3; }在堆上创建一个新对象, 并把新对象的引用赋值给 a , Main方法中 a1 仍然是之前的对象, 没有同 a 一起改变, ChangeA方法结束后创建的新对象不再被任何变量引用, 可以被回收. 上述传参方法实际上把对 a1 的引用(指针)赋值给了 a, a1 与 a 仍然是两个不同的变量. 可以对类类型使用ref修饰符传递对引用的引用(二级指针)来修改 a1:

public static void ChangeA(ref A a)
{
    a.X = 2;
    a = new A { X = 3; }
}
static void Main()
{
    A a1 = new A { X = 1; };
    ChangeA(ref a1);
    Console.WriteLine($"{a1.X}");	//output: 3
}

out 参数

​ 如果方法返回多个值, 可能类型还不同, 这时有不同的选项可以实现. 一个选项是声明类和结构, 将返回的数据定义为成员; 另一个选项是使用元组类型; 第三个选项是使用out关键字.

Int32类的成员方法TryParse用于将字符串转化为int数据. 无论成功与否, 该方法都返回一个bool值. 如果成功, 解析的结果使用out参数返回.

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

在调用方法之前, 对out参数传递的变量只需声明而无需赋值, 即使赋值也会在方法中被覆盖掉, 因为out参数必须在方法内为其赋值.

  • 在 C# 7 前的版本必须提前声明一个 out 变量; 之后的版本中可以由调用方法实现变量的声明
  • 如果类型是由方法签名明确定义的, 则可以使用var关键字(即在调用时使用out var result的参数写法实现 out 参数的定义与传参)

​ 与ref相同, 在声明方法与调用方法的参数列表中都需要使用out关键字修饰.

string input = ReadLine();
if(int.TryParse(input, out int result))
{
    Console.WriteLine(${result: {result}});
}
else 
{
    Console.WriteLine("not a number");
}

in 参数

in修饰符保证发送到方法中的数据不会更改(在传递值类型时). in修饰符使参数被设置为只读变量.

  • 类似于C++的const参数
static void CannotChange(in AValueType a)
{
    //a.Data = 43;	//compile error. a is readonly variable.
    Console.WriteLine(a.Data);
}
  • in修饰符主要用于值类型. 也可以对引用类型使用它, 这时可以更改引用对象的内容, 但不能更改变量本身(即引用本身)

可空类型

​ 引用类型变量可以为空, 而值类型不能. 当把数据库或 XML 类型映射到 C# 类型时这可能是个问题. 数据库或 XML 类型可空, 而 int 或 double 不能. C# 为此提供了一个解决方案: 可空类型. ​ 可空类型是可以为空的值类型. 要声明可空类型只需在值类型后添加?. 与基本结构相比, 可空类型的唯一开销是一个确定是否为空的bool成员.

int x1 = 0;
int? x2 = null;

​ 因为int值可以分配给int?, 所以int?传递给一个int总是会成功, 编译器会接受它:

int? x3 = x1;

​ 但反过来是不正确的. int?不能直接分配给int, 这可能失败, 因此需要一个类型转换:

int x4 = (int)x3;

​ 但如果x3是 null, 类型转换操作就会抛出异常. ​ 更好的方法是使用可空类型的HasValueValue属性. HasValue返回truefalse, 这取决于可空类型是否有值; Value返回底层的值.

int x5 = x3.HasValue ? x3.Value : -1;
使用合并操作符??, 可空类型可以使用较短的语法. 如果x3是 null, 则用x6给他设置 -1, 否则提取x3的值:
int x6 = x3 ?? -1;

## 枚举类型

​ 枚举是值类型, 包含一组命名的常量. 枚举类型使用enum关键字定义.

public enum Color
{
    Red, Green, Blue	//value: 0, 1, 2
}

​ 可以声明枚举类型的变量. 用枚举类型名称作为前缀设置一个命名常量, 来赋予枚举中的一个值.

Color c1 = Color.Red;
Console.WriteLine(c1);	//output: Red

​ 默认情况下, enum 的类型是 int. 这个基本类型可以改为其他整数类型(byte, short, int, long 和无符号变量). 默认情况下命名常量的值从 0 开始递增, 但可以改为其他值.

public enum Color : short
{
    Red = 1,Green, Blue	//value: 1, 2, 3
}

​ 使用强制类型转换可以把数字改为枚举值, 把枚举值改为数字.

Color c2 = (Color)2;
short number = (short)c2;	//必须显式写出类型转换

​ 使用[Flags]属性可以使 enum 类型把多个选项分配给一个变量. 为此, 分配给常量的值必须是不同的位.

[Flags]
public enum DaysOfWeek
{
    Monday = 0x1,
    Tuesday = 0x2,
    Wednesday = 0x4,
    Thursday = 0x8,
    Friday = 0x10,
    Saturday = 0x20,
    Sunday = 0x40,
    Weekend = Saturday | Sunday,
    Weekday = 0x1f
}

​ 有了如上定义, 就可以把多个枚举值使用按位或运算结合后赋值给枚举变量. 输出时不会显示实际数值, 而是字符串表示.

DaysOfWeek a = DaysOfWeek.Monday | DaysOfWeek.Tuesday;
Console.WriteLine(a);	//output: Monday, Tuesday
DaysOfWeek b = DaysOfWeek.Saturday | DaysOfWeek.Sunday;
Console.WriteLine(b);	//output: Weekend
DaysOfWeek c = (DaysOfWeek)0b10001;
Console.WriteLine(c);	//output: Monday, Friday

​ 类Enum有助于动态获得枚举类型信息. Enum类提供了方法来解析字符串, 获得相应的枚举名称和值. ​ 下面代码使用Enum.TryParse<T>方法来获得字符串对应枚举值.

Color red;
if(Enum.TryParse<Color>("Red", out red))
{
    Console.WriteLine($"successfully parsed {red}.");
}	//output: successfully parsed Red.

Enum.GetNames方法返回一个包含所有枚举名的字符串数组.

foreach(var dye in Enum.GetNames(typeof(Color)))	// output:
{												// Red
	Console.WriteLine(dye);						  // Green
}												// Blue

Enum.GetValues方法返回枚举数值的数组.

foreach(int val in Enum.GetValues(typeof(Color)))
{
	Console.WriteLine(val);
}

部分类

partial关键字允许把类, 结构, 方法或接口放在多个文件中. partial关键字的用法是放在class struct interface关键字的前面.

// SampleClassAutoGenerated.cs
partial class SampleClass
{
    public void MethodOne() { }
}

// SampleClass.cs
partial class SampleClass
{
    public void MethodTwo() { }
}

当编译包含这两个源文件的项目时, 会创建一个SampleClass类, 它有两个方法MethodOne()MethodTwo().

​ 如果声明类时使用了下面关键字, 则这些关键字就必须应用于同一个类的所有部分:

  • public
  • private
  • protected
  • internal
  • abstract
  • sealed
  • new
  • 一般约束

​ 在嵌套的类型中, 只要partial关键字位于class关键字前, 就可以嵌套部分类. 在把部分类编译到类型中时, 属性, XML 注释, 接口, 泛型类型的参数属性和成员会合并. 有如下两个源文件:

// SampleClassAutogenerated.cs
[CustomAttribute]
partial class SampleClass: SampleBaseClass, ISampleClass
{
    public void MethodOne() { }
}

// SampleClass.cs
[AnotherAttribute]
partial class SampleClass: SampleBaseClass, ISampleClass, IOtherSampleClass
{
    public void MethodTwo() { }
}

将这两个源文件编译后, 等价的源文件是:

[CustomAttribute]
[AnotherAttribute]
partial class SampleClass: SampleBaseClass, ISampleClass, IOtherSampleClass
{
    public void MethodOne() { }
    public void MethodTwo() { }
}
可以用partial关键字创建部分方法. 部分方法的实现可以放在部分类的任何地方. 扩展部分类的程序员可以决定创建部分方法的实现代码, 或者什么也不做. 如果生成的代码应该调用可能不存在的方法, 使用部分方法将会有帮助. 如果部分方法的返回值为void, 且没有实现代码(只有声明), 编译器将删除这个方法调用.
// SampleClassAutogenerated.cs
partial class SampleClass
{
    public void Method()
    {
        APartialMethod();
    }
    public partial void APartialMethod(); 
}

如果只编译这一个源文件, 那么编译器将删除对APartialMethod()的调用;

// SampleClass.cs
partial class SampleClass
{
    //do something...
}
  • 部分方法返回值必须是void类型, 否则编译器无法在没有实现代码的情况下删除调用.

使用扩展方法扩展类

​ 扩展方法是给对象添加功能的一个选项(其他选项有继承等). 在不能使用继承时(例如类是密封的), 可以考虑使用这个选项.

  • 扩展方法也可以用于扩展接口. 这样, 实现该接口的所有类就有了公共功能.

​ 扩展方法是静态方法, 它是类的一部分, 但实际没有放在类的源代码中. ​ 假设希望为 string 类型拓展GetWordCount()方法用于计算字符串中的单词数:

public static class StringExtension
{
    public static int GetWordCount(this string s) => s.Split().Length;
}

使用this关键字和第一个参数来扩展字符串. 这个关键字定义了要扩展的类型.

string fox = "the quick brown fox jumped over the lazy dogs down " + "9876543210 times";
int wordCount = fox.GetWordCount();
Console.WriteLine(wordCount);	//output: 12

​ 即使扩展方法是静态的, 也要使用实例调用扩展方法而不是使用类型. 注意, 调用GetWordCount()方法的是实例 fox 而不是类型名 string. ​ 扩展方法只能定义在顶级静态类中, 而不能定义在嵌套类中. ​ 若出现相同签名方法, 被扩展类总是优先使用类中已有的实例方法; 若有同签名扩展方法位于不同命名空间, 被扩展类优先使用与自己在同一个命名空间的扩展方法; 若扩展方法与被扩展类位于不同命名空间, 除非使用using指令打开扩展方法所在命名空间, 否则编译器无法找到该扩展方法; 当扩展一个类的多个同名扩展方法的命名空间被同时打开时, 编译器会产生一个编译错误, 指出调用是不明确的.

Object 类

​ 所有的 .NET 类型最终都派生自System.Object. 实际上, 若定义类时不指定基类, 则这个类默认派生自Object. 对于结构, 这个派生是间接的: 结构总是派生自System.ValueType, System.ValueType又派生自System.Object. ​ 其实际意义在于: 除了自己定义的方法和属性等成员外, 还可以访问Object类定义的许多公有的和受保护的成员方法.

方法 作用
Equals(Object) 确定指定对象是否等于当前对象.
Equals(Object, Object) 确定指定的对象实例是否被视为相等.
Finalize() 在垃圾回收将某一对象回收前允许该对象尝试释放资源并执行其他清理操作.
GetHashCode() 作为默认哈希函数.
GetType() 获取当前实例的 Type.
MemberwiseClone() 创建当前 Object 的浅表副本(即复制类的所有值类型成员和引用成员, 而不复制引用成员指向的对象). 该方法不是虚方法, 所以不可重写.
ReferenceEquals(Object, Object) 确定指定的 Object 实例是否是相同的实例.
ToString() 返回表示当前对象的字符串.