C#8.0本质论第六章--类

C#8.0本质论第六章–类

类可以理解为对象的模板。面向对象编程的一个关键优势就是不必从头创建新程序,而是可以将现有的一系列对象组装到一起。

6.1类的声明和实例化

虽然并非必须,但一般应该将每个类都放到他自己的文件中,用类名对文件进行命名。

虽然有new操作符分配内存,但没有对应的操作符回收内存,具体由垃圾回收器回收。

程序员应将new的作用理解成实例化对象而不是分配内存。在堆和栈上分配对象都支持new操作符,这进一步强调了new不是关于内存分配的,也不是关于是否有必要进行回收的。

6.2实例字段

在面向对象术语中,在类中存储数据的变量称为成员变量,这个术语在C#中很好理解,但更标准更符合规范的术语是字段

6.2.1声明实例字段
6.2.2访问实例字段

6.3实例方法

6.4使用this关键字

this是隐含的,它返回对象本身的实例。

6.5访问修饰符

封装的另一个重要作用是隐藏对象的数据和行为的内部细节。访问修饰符的作用是提供封装

6.6属性

6.6.1声明属性

属性的关键在于,它提供了从编程角度看类似于字段的API。定义使用了三个上下文关键字,其中get和set关键字分别表示属性的取值和赋值部分,赋值方法可用value关键字引用赋值操作的右侧部分。

    // LastName property
    public string LastName
    {
        get => _LastName;
        set => _LastName = value;
    }
    private string _LastName;
6.6.2自动实现的属性

C#3.0起有了属性语法简化版本。

    // Title property
    public string? Title { get; set; }
 
    // Manager property
    public Employee? Manager { get; set; }
 
    public string? Salary { get; set; } = "Not Enough";

从C#6.0开始可以在声明时进行初始化:

public string? Salary { get; set; } = "Not Enough";
6.6.3属性和字段的设计规范

为符合封闭原则,属性的支持字段不应声明为public或protected。

要优先使用自动实现的属性而不是自己写的完整的版本,也不是字段。

6.6.4提供属性验证

一个好的实践是只从属性的实现中访问属性的支持字段。因为属性可能包含了一些验证代码。

虽然少见,但确实能在赋值方法中对value进行赋值。(说明value是左值?)

nameof操作符获取一个标识符作为参数,返回该名称的字符串形式,nameof的优点在于,以后若标识符发生改变,重构工具能自动修改实参。

6.6.5只读和只写属性

不写get只写set就是只写,不写set只写get就是只读。

属性如果不加get访问器,那么在哪里都不能获取它的值,如果在get前加private,那么在该类里可以获取,在其他地方则不能获取。

6.6.6属性作为虚字段
6.6.7取值和赋值方法的访问修饰符
6.6.8属性和方法调用不允许作为ref和out参数值

ref和out参数内部要将内存地址传给目标方法。但由于属性可能是无支持字段的虚字段,也有可能只读或只写,所以不可能传递存储地址,同样适用于方法调用(个人觉得是右值的原因,在C#里好像没这种说法,虽然属性表现的像字段,但更像一个方法)。

6.7构造函数

6.7.1声明构造函数

new操作符的实现细节:new从内存管理器获取“空白”内存,调用指定构造函数,将对“空白”内存的引用作为隐式的this参数传给构造函数。构造函数链剩余部分开始执行,在构造函数之间传递引用。这些构造函数没有返回类型。构造函数链上的执行结束后,new返回内存引用。该引用指向的内存处于完成初始化的形态。

6.7.2默认构造函数

如果类没有显示定义的构造函数,C#编译器会在编译时自动添加一个。称为默认构造函数。一旦为类显式添加了构造函数,就不在添加默认构造函数。

6.7.3对象初始化器

C#3.0新增了对象初始化器,创建对象时可在后面一对大括号中添加成员初始化列表。这实际是一种语法糖,最终生成的CIL代码和创建对象后单独对字段赋值无异。

    public static void Main()
    {
        Employee employee = new("Inigo", "Montoya") 
            { Title = "Computer Nerd", Salary = "Not enough" };
        // ...
    }

C#3.0还增加了集合初始化器

    public static void Main()
    {
        List<Employee> employees = new()
            {
                new("Inigo", "Montoya"),
                new("Kevin", "Bost")
            };
        // ...
    }

为定义在对象销毁过程中发生的事情,C#提供了终结器。和C++析构函数不同,终结器不是在对一个对象的所有引用都消失后马上运行。相反,终结器是在对象被判定“不可到达”之后的不确定时间内执行。

6.7.4重构构造函数

从C#7.0开始支持构造函数的表达式主体成员实现。

public Employee(int id) => Id = id;
6.7.5构造函数链:使用this调用另一个构造函数

对对象进行初始化的代码在好几个地方重复,所以必须在多个地方维护,但完全可以从一个构造函数中调用另一个构造函数。这称为构造函数链,用构造函数初始化器实现,它会在执行当前构造函数实现前,判断要调用另外哪一个构造函数。

    public Employee(int id, string firstName, string lastName) : this(firstName, lastName)
    {
        Id = id;
    }

6.8不可空引用类型属性与构造函数

6.8.1可读写的引用型不可空属性
6.8.2自动实现的只读引用型属性

6.9可空特性

6.8-6.9待补充…

6.10解构函数

构造函数允许获取多个参数并把它们全部封装到一个对象中。但在C#7.0之前没有一个显式的语言构造来做相反的事情。自C#7.0推出元组语法后该操作得到极大简化。

public class Employee
{
    // ...
    public void Deconstruct(out int id, out string firstName, out string lastName, out string? salary)
    {
       (id, firstName, lastName, salary) = (Id, FirstName, LastName, Salary);
    }
    // ...
}
 
public class Program
{
    public static void Main()
    {
        Employee employee;
        employee = new ("Inigo", "Montoya")
        {
            // Leveraging object initializer syntax
            Salary = "Too Little"
        };
        // ...
 
        employee.Deconstruct(out _, out string firstName,
            out string lastName, out string? salary);
    }
}

调用前要以内联形式声明out参数,也就是必须要带out。

从C#7.0开始可直接将对象实例赋给一个元组,从而隐式调用Deconstruct()方法(解构函数)。

注意,只允许用元组语法向那些和out参数匹配的遍历赋值。不允许向元组类型的遍历赋值。

//可以
(_, string firstName, string lastName, string? salary) = employee;
//下面都不行
(int, string, string, string) tuple = employee;
(int id, string firstName, string lastName, string salary) tuple = employee

为声明解构函数,方法名必须是Deconstruct,签名为返回void,并接收两个或更多out参数。(如果显式调用,其实可以不必为Deconstruct,就当作是一个普通函数了,但此时不能用元组赋值了)(另外,一个out参数好像是不行的,此时也没有必要了)

6.11静态成员

6.11.1静态字段
6.11.2静态方法
6.11.3静态构造函数

C#支持静态构造函数,用于对类(而不是实例)进行初始化,不显式调用,"运行时"在首次访问类时自动调用静态构造函数。

由于静态构造函数不能显式调用,所以不允许任何参数。静态构造函数的作用是将类中的静态数据初始化成特定值,比如涉及方法调用时,没办法在声明时直接进行赋值。

如果在静态构造函数中赋值,同时又在声明时赋值,那么最终获得什么值?观察CIL代码,发现声明时的赋值被移动了位置,成为静态构造函数中第一个语句。(也就是说先初始化静态成员,再执行静态构造函数)这和实例字段的情况一样。

不要在静态构造函数中抛出异常,否则会造成类型在应用程序剩余的生存期内无法使用。

最好在声明时进行静态初始化,而不要用静态构造函数。

书本原话:

Favor Static Initialization during Declaration

Static constructors execute before the first access to any member of a class, whether it is a static field, another static member, or an instance constructor. To support this practice, the runtime checks that all static members and constructors to ensure that the static constructor runs first.

Without the static constructor, the compiler initializes all static members to their default values and avoids adding the static constructor check. The result is that static assignment initialization is called before any static fields are accessed but not necessarily before all static methods or any instance constructor is invoked. This might provide a performance improvement if initialization of static members is expensive and is not needed before accessing a static field. For this reason, you should consider either initializing static fields inline rather than using a static constructor or initializing them at declaration time.

6.11.4静态属性
6.11.5静态类

有的类不包含任何实例字段,创建能实例化的类没有意义,所以用static关键字修饰该类。声明时用static有两方面的意义。首先,它防止程序员写代码来实例化,其次,防止在类的内部声明任何实例字段或方法。

C#编译器自动在CIL代码把它标记为abstract和sealed,这将指定为不可扩展。

6.12扩展方法

C#3.0引入了扩展方法的概念,能模拟为其他类创建实例方法。必须在静态类的静态方法中,使第一个参数成为要扩展的类型,并在类型前加this关键字,查看CIL代码发现扩展方法是作为普通静态方法调用的。

如果扩展方法的签名和扩展类型中现有的签名匹配,扩展方法永远不会得到调用,除非作为普通静态方法。

通过继承来特化类型要优于使用扩展方法,扩展方法无益于建立清楚的版本控制机制,因为一旦在被扩展类型中添加匹配的签名,就会覆盖现有扩展方法,而不会发出警告。另一个问题是,VS的智能感知支持扩展方法,如果只是查看代码,是不易看出一个方法是不是扩展方法的。因此扩展方法要慎用。

6.13封装数据

6.13.1const

const字段包含在编译时确定的值,运行时不可修改。

常量字段自动成为静态字段,因为不需要为每个对象实例都生成新的字段实例。但将常量字段显式声明为static会造成编译错误。常量字段只能声明成有字面值的类型。

public常量应恒定不变,如果修改它,在使用它的程序集中不一定能反应出最新变化。如果一个程序集应用了另一个程序集中的常量,那么常量将直接编译到引用程序中。所以如果被引用程序集中的值发生改变,而引用程序集没有重新编译,那么引用程序集将继续使用原始值而非新值。

6.13.2readonly

和const不同,readonly修饰符只能用于字段,指出字段值只能从构造函数中更改,或在声明时通过初始化器指定。

readonly字段可以是实例或静态字段。关键区别是可在执行时为readonly字段赋值。

另一个重要特点是不限于有字面值的类型。

由于规范要求字段不雅从起包容属性外部访问,所以从C#6.0起readonly修饰符几乎完全没有了用武之地。相反,总是选择只读自动实现属性就可以了。

6.14嵌套类

在类中可以定义另一个类,这种类称为嵌套类,嵌套类可以访问包容类的任何成员,包括私有成员,反之则不然。

避免声明公共嵌套类,这是不良的编码风格,可能造成混淆和难以阅读。

6.15分布类

从C#2.0开始支持分布类,分布类是一个类的多个部分,编译器可把它们合并成一个完整的类,目的就是将一个类的定义划分到多个文件中,这对生成或修改代码的工具尤其有用,

6.15.1定义分布类
// File: Program1.cs
partial class Program
{
}
// File: Program2.cs
partial class Program
{
}

除了用于代码生成器,另一个常见应用是将每个嵌套类都放到它们自己的文件中。

不允许用分布类扩展编译好的类或其他程序集中的类。

6.15.2分布方法

C#3.0引入分布方法的概念,只存在于分布类中,主要作用是为代码生成提供方便。

假如代码生成工具能根据数据库中的表为类生成对应的文件,我们不好直接修改,假如重新生成了,所做的更改就会丢失。相反,应时生成的代码在一个文件里,自定义代码在另一个文件里。

分布方法允许声明方法而不需要实现。

// File: Person.Designer.cs
public partial class Person
{
    #region Extensibility Method Definitions
    static partial void OnLastNameChanging(string value);
    static partial void OnFirstNameChanging(string value);
    #endregion Extensibility Method Definitions
 
    // ...
    public string LastName
    {
        get
        {
            return _LastName;
        }
        set
        {
            if (_LastName != value)
            {
                OnLastNameChanging(value);
                _LastName = value;
            }
        }
    }
    private string _LastName;
 
    // ...
    public string FirstName
    {
        get
        {
            return _FirstName;
        }
        set
        {
            if (_FirstName != value)
            {
                OnFirstNameChanging(value);
                _FirstName = value;
            }
        }
    }
    private string _FirstName;
 
    public partial string GetName();
}
 
// File: Person.cs
partial class Person
{
    static partial void OnLastNameChanging(string value)
    {
        value = value ?? 
            throw new ArgumentNullException(nameof(value));
 
        if (value.Trim().Length == 0)
        {
            throw new ArgumentException(
                $"{nameof(LastName)} cannot be empty.",
                nameof(value));
        }
    }
 
    // ...
 
    public partial string GetName() => $"{FirstName} {LastName}";
}

分布方法必须返回void,out参数在分布方法中不允许,需要返回值可以用ref参数。分布方法使生成的代码能调用并非一定要实现的方法。

猜你喜欢

转载自blog.csdn.net/Story_1419/article/details/133235708
今日推荐