Class&Struct

至页首    至页尾

1.类与结构

类是引用类型
创建类的对象后,向其分配对象的变量仅保留对相应内存的引用。 将对象引用分配给新变量后,新变量会引用原始对象。 通过一个变量所做的更改将反映在另一个变量中,因为它们引用相同的数据。


结构是值类型
创建结构时,向其分配结构的变量保留结构的实际数据。 将结构分配给新变量时,会复制结构。 因此,新变量和原始变量包含相同数据的副本(共两个)。 对一个副本所做的更改不会影响另一个副本。


一般来说,类用于对更复杂的行为或应在类对象创建后进行修改的数据建模。 结构最适用于所含大部分数据不得在结构创建后进行修改的小型数据结构。

面向对象三大特性:

  • 封装:封装就是隐藏对象的属性和实现细节,暴露出外部访问的接口,通过可访问修饰符控制在程序中属性的读取和修改的访问级别。
  • 继承:通过继承,可以创建重用、扩展和修改在其他类(基类)中定义的行为的新类。
  • 多态:在同一个方法中,由于参数不同而导致执行效果各异的现象就是多态。通过函数重载,抽象类,虚方法实现。

2.类

2.1 引用类型

定义为类的一个类型是引用类型。 在运行时,如果声明引用类型的变量,此变量就会一直包含值 null,直到使用 new 运算符显式创建类实例,或直到为此变量分配可能已在其他位置创建的兼容类型的对象,如下面的示例所示:

//Declaring a object of type MyClass.
MyClass mc = new MyClass();

//Declaring another object of the same type, assigning it the value of the first object.
MyClass mc2 = mc;

2.2 声明类

使用后跟唯一标识符的 class 关键字可以声明类,如下例所示:

//[access modifier] - [class] - [identifier]
public class Customer
{
   // Fields, properties, methods and events go here...
}

2.3 创建对象

可通过使用 new 关键字,后跟对象要基于的类的名称,来创建对象,如:

Customer object1 = new Customer();

Customer object2 = new Customer();
Customer object3 = object3;

2.4 继承类

创建类时,可以继承自其他任何未定义为 sealed 的接口或类,而且其他类也可以继承自你的类并重写类虚方法。


继承是通过使用派生来完成的,这意味着类是通过使用其数据和行为所派生自的基类来声明的。 基类通过在派生的类名称后面追加冒号和基类名称来指定,如:

public class Manager : Employee
{
    // Employee fields, properties, methods and events are inherited
    // New Manager fields, properties, methods and events go here...
}

3.对象

3.1 类实例与结构实例

由于类是引用类型,因此类对象的变量引用该对象在托管堆上的地址。 如果将同一类型的第二个对象分配给第一个对象,则两个变量都引用该地址的对象。


类的实例是使用 new 运算符创建的。 在下面的示例中,Person 为类型,person1 和 person 2 为该类型的实例(即对象)。

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
    //Other properties, methods, events...
}

class Program
{
    static void Main()
    {
        Person person1 = new Person("Leopold", 6);
        Console.WriteLine("person1 Name = {0} Age = {1}", person1.Name, person1.Age);

        // Declare  new person, assign person1 to it.
        Person person2 = person1;

        //Change the name of person2, and person1 also changes.
        person2.Name = "Molly";
        person2.Age = 16;

        Console.WriteLine("person2 Name = {0} Age = {1}", person2.Name, person2.Age);
        Console.WriteLine("person1 Name = {0} Age = {1}", person1.Name, person1.Age);

        // Keep the console open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();

    }
}
/*
    Output:
    person1 Name = Leopold Age = 6
    person2 Name = Molly Age = 16
    person1 Name = Molly Age = 16
*/

由于结构是值类型,因此结构对象的变量具有整个对象的副本。 结构的实例也可以使用 new 运算符来创建,但这不是必需的,如下面的示例所示:

public struct Person
{
    public string Name;
    public int Age;
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

public class Application
{
    static void Main()
    {
        // Create  struct instance and initialize by using "new".
        // Memory is allocated on thread stack.
        Person p1 = new Person("Alex", 9);
        Console.WriteLine("p1 Name = {0} Age = {1}", p1.Name, p1.Age);

        // Create  new struct object. Note that  struct can be initialized
        // without using "new".
        Person p2 = p1;

        // Assign values to p2 members.
        p2.Name = "Spencer";
        p2.Age = 7;
        Console.WriteLine("p2 Name = {0} Age = {1}", p2.Name, p2.Age);

        // p1 values remain unchanged because p2 is  copy.
        Console.WriteLine("p1 Name = {0} Age = {1}", p1.Name, p1.Age);

        // Keep the console open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
/*
  Output:
    p1 Name = Alex Age = 9
    p2 Name = Spencer Age = 7
    p1 Name = Alex Age = 9
*/

3.2 对象标识与值相等性

在比较两个对象是否相等时,首先必须明确是想知道两个变量是否表示内存中的同一对象,还是想知道这两个对象的一个或多个字段的值是否相等。 如果要对值进行比较,则必须考虑这两个对象是值类型(结构)的实例,还是引用类型(类、委托、数组)的实例。

  • 若要确定两个类实例是否引用内存中的同一位置(这意味着它们具有相同的标识),可使用静态 Equals 方法。 (System.Object 是所有值类型和引用类型的隐式基类,其中包括用户定义的结构和类。)
  • 若要确定两个结构实例中的实例字段是否具有相同的值,可使用 ValueType.Equals 方法。 由于所有结构都隐式继承自 System.ValueType,因此可以直接在对象上调用该方法,如以下示例所示:
// struct Person is defined in the previous example.

Person p1 = new Person("Wallace", 75);
Person p2;
p2.Name = "Wallace";
p2.Age = 75;

if (p2.Equals(p1))
    Console.WriteLine("p2 and p1 have the same values.");

// Output: p2 and p1 have the same values.

4.结构

通过使用struct关键字来定义结构,例如:

public struct PostalAddress
{
    // Fields, properties, methods and events go here...
}

结构与类的大部分语法相同。 结构名称必须是有效的 C# 标识符名称。 结构在以下方面比类的限制更多:

  • 在结构声明中,除非将字段声明为 const 或 static,否则无法初始化。
  • 结构不能声明默认构造函数(没有参数的构造函数)或终结器。
  • 结构在分配时进行复制。 将结构分配给新变量时,将复制所有数据,并且对新副本所做的任何修改不会更改原始副本的数据。
  • 结构是值类型,不同于类,类是引用类型。
  • 与类不同,无需使用 new 运算符即可对结构进行实例化。
  • 结构可以声明具有参数的构造函数。
  • 一个结构无法继承自另一个结构或类,并且它不能为类的基类。 所有结构都直接继承自 ValueType,后者继承自 Object。
  • 结构可以实现接口。
  • 结构可用作可以为 null 的类型,并且可以向其分配一个 null 值。

4.1 使用结构

struct 类型适用于表示轻量级对象,如 Point、 Rectangle和 Color。 尽管用它来表示一个点就如同具有 Auto-Implemented Properties(自动实现的属性) 的 类那样方便,但在某些情况下,使用 结构 可能更高效。 例如,如果你声明具有 1000 个 Point 对象的数组,那么你将分配额外的内存用于引用每个对象;在这种情况下,使用结构将更为便宜。 因为 .NET Framework 包含一个称为 Point的对象,因此在此示例中的结构改名为“CoOrds”。

public struct CoOrds
{
    public int x, y;

    public CoOrds(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

5.继承

派生类只能有一个直接基类。 但是,继承是可传递的。 如果 ClassC 派生自 ClassB,并且 ClassB 派生自 ClassA,则 ClassC 会继承在 ClassB 和 ClassA 中声明的成员。


从概念上讲,派生类是基类的专门化。 例如,如果有一个基类 Animal,则可以有一个名为 Mammal 的派生类,以及另一个名为 Reptile 的派生类。 Mammal 是 Animal,Reptile 也是 Animal,但每个派生类表示基类的不同专门化。


定义要从其他类派生的类时,派生类会隐式获得基类的所有成员(除了其构造函数和终结器)。 派生类因而可以重用基类中的代码,而无需重新实现。 在派生类中,可以添加更多成员。 通过这种方法,派生类可扩展基类的功能。

// WorkItem implicitly inherits from the Object class.
public class WorkItem
{
    // Static field currentID stores the job ID of the last WorkItem that
    // has been created.
    private static int currentID;

    //Properties.
    protected int ID { get; set; }
    protected string Title { get; set; }
    protected string Description { get; set; }
    protected TimeSpan jobLength { get; set; }

    // Default constructor. If a derived class does not invoke a base-
    // class constructor explicitly, the default constructor is called
    // implicitly. 
    public WorkItem()
    {
        ID = 0;
        Title = "Default title";
        Description = "Default description.";
        jobLength = new TimeSpan();
    }

    // Instance constructor that has three parameters.
    public WorkItem(string title, string desc, TimeSpan joblen)
    {
        this.ID = GetNextID();
        this.Title = title;
        this.Description = desc;
        this.jobLength = joblen;
    }

    // Static constructor to initialize the static member, currentID. This
    // constructor is called one time, automatically, before any instance
    // of WorkItem or ChangeRequest is created, or currentID is referenced.
    static WorkItem()
    {
        currentID = 0;
    }


    protected int GetNextID()
    {
        // currentID is a static field. It is incremented each time a new
        // instance of WorkItem is created.
        return ++currentID;
    }

    // Method Update enables you to update the title and job length of an
    // existing WorkItem object.
    public void Update(string title, TimeSpan joblen)
    {
        this.Title = title;
        this.jobLength = joblen;
    }

    // Virtual method override of the ToString method that is inherited
    // from System.Object.
    public override string ToString()
    {
        return String.Format("{0} - {1}", this.ID, this.Title);
    }
}

// ChangeRequest derives from WorkItem and adds a property (originalItemID) 
// and two constructors.
public class ChangeRequest : WorkItem
{
    protected int originalItemID { get; set; }

    // Constructors. Because neither constructor calls a base-class 
    // constructor explicitly, the default constructor in the base class
    // is called implicitly. The base class must contain a default 
    // constructor.

    // Default constructor for the derived class.
    public ChangeRequest() { }

    // Instance constructor that has four parameters.
    public ChangeRequest(string title, string desc, TimeSpan jobLen,
                         int originalID)
    {
        // The following properties and the GetNexID method are inherited 
        // from WorkItem.
        this.ID = GetNextID();
        this.Title = title;
        this.Description = desc;
        this.jobLength = jobLen;

        // Property originalItemId is a member of ChangeRequest, but not 
        // of WorkItem.
        this.originalItemID = originalID;
    }
}

class Program
{
    static void Main()
    {
        // Create an instance of WorkItem by using the constructor in the 
        // base class that takes three arguments.
        WorkItem item = new WorkItem("Fix Bugs",
                                     "Fix all bugs in my code branch",
                                     new TimeSpan(3, 4, 0, 0));

        // Create an instance of ChangeRequest by using the constructor in
        // the derived class that takes four arguments.
        ChangeRequest change = new ChangeRequest("Change Base Class Design",
                                                 "Add members to the class",
                                                 new TimeSpan(4, 0, 0),
                                                 1);

        // Use the ToString method defined in WorkItem.
        Console.WriteLine(item.ToString());

        // Use the inherited Update method to change the title of the 
        // ChangeRequest object.
        change.Update("Change the Design of the Base Class",
            new TimeSpan(4, 0, 0));

        // ChangeRequest inherits WorkItem's override of ToString.
        Console.WriteLine(change.ToString());

        // Keep the console open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
/* Output:
    1 - Fix Bugs
    2 - Change the Design of the Base Class
*/

6.多态

6.1 多态性概述

6.1.1 虚成员

当派生类从基类继承时,它会获得基类的所有方法、字段、属性和事件。 派生类的设计器可以选择

  • 是否重写基类中的虚拟成员。(可以不重写)
  • 继承最接近的基类方法而不重写它
  • 定义隐藏基类实现的成员的新非虚实现
    仅当基类成员声明为 virtual 或 abstract 时,派生类才能重写基类成员。 派生成员必须使用 override 关键字显式指示该方法将参与虚调用。 以下代码提供了一个示例:
public class BaseClass
{
    public virtual void DoWork() { }
    public virtual int WorkProperty
    {
        get { return 0; }
    }
}
public class DerivedClass : BaseClass
{
    public override void DoWork() { } // 覆盖基类虚方法
    public override int WorkProperty
    {
        get { return 0; }
    }
}

字段不能是虚拟的,只有方法、属性、事件和索引器才可以是虚拟的。 当派生类重写某个虚拟成员时,即使该派生类的实例被当作基类的实例访问,也会调用该成员。 以下代码提供了一个示例:

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Also calls the new method.

虚方法和属性允许派生类扩展基类,而无需使用方法的基类实现。 接口提供另一种方式来定义将实现留给派生类的方法或方法集。

6.1.2 使用新成员隐藏基类成员

如果希望派生成员具有与基类中的成员相同的名称,但又不希望派生成员参与虚调用,则可以使用 new 关键字。 new 关键字放置在要替换的类成员的返回类型之前。 以下代码提供了一个示例:

public class BaseClass
{
    public void DoWork() { WorkField++; }
    public int WorkField;
    public int WorkProperty
    {
        get { return 0; }
    }
}

public class DerivedClass : BaseClass
{
    public new void DoWork() { WorkField++; } // 不覆盖基类虚方法
    public new int WorkField;
    public new int WorkProperty
    {
        get { return 0; }
    }
}

通过将派生类的实例强制转换为基类的实例,仍然可以从客户端代码访问隐藏的基类成员。 例如:

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Calls the old method.
6.1.3 阻止派生类重写虚拟成员

无论在虚拟成员和最初声明虚拟成员的类之间已声明了多少个类,虚拟成员永远都是虚拟的。 如果类 A 声明了一个虚拟成员,类 B 从 A 派生,类 C 从类 B 派生,则类 C 继承该虚拟成员,并且可以选择重写它,而不管类 B 是否为该成员声明了重写。 以下代码提供了一个示例:

public class A
{
    public virtual void DoWork() { }
}
public class B : A
{
    public override void DoWork() { }
}

派生类可以通过将重写声明为 sealed 来停止虚拟继承。 这需要在类成员声明中的 override 关键字前面放置 sealed 关键字。 以下代码提供了一个示例:

public class C : B
{
    public sealed override void DoWork() { }
}

在上一示例中,方法 DoWork 对从 C 派生的任何类都不再是虚方法。它对 C 的实例仍是虚拟的,即使它们转换为类型 B 或类型 A。使用 new 关键字可以将密封方法替换为派生类,如下方示例所示:

public class D : C
{
    public new void DoWork() { }
}

在此情况下,如果在 D 中使用类型为 D 的变量调用 DoWork,被调用的将是新的 DoWork。 如果使用类型为 C、B 或 A 的变量访问 D 的实例,对 DoWork 的调用将遵循虚拟继承的规则,即把这些调用传送到类 C 的 实现。

6.1.4 从派生类访问基类虚拟成员

已替换或重写某个方法或属性的派生类仍然可以使用基关键字访问基类的该方法或属性。 以下代码提供了一个示例:

public class Base
{
    public virtual void DoWork() {/*...*/ }
}
public class Derived : Base
{
    public override void DoWork()
    {
        //Perform Derived's work here
        //...
        // Call DoWork on base class
        base.DoWork();
    }
}

6.2 重写 ToString 方法

声明具有下列修饰符和返回类型的 ToString 方法:

public override string ToString(){}  

实现该方法,使其返回一个字符串。

下面的示例返回类的名称,但特定于该类的特定实例的数据除外。

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        return "Person: " + Name + " " + Age;
    }
}

可以测试 ToString 方法,如以下代码示例所示:

Person person = new Person { Name = "John", Age = 12 };
Console.WriteLine(person);
// Output:
// Person: John 12

7.抽象类、密封类及类成员

使用 abstract 关键字可以创建不完整且必须在派生类中实现的类和 class 成员。

使用 sealed 关键字可以防止继承以前标记为 virtual 的类或某些类成员。

7.1 抽象类和类成员

通过在类定义前面放置关键字 abstract,可以将类声明为抽象类。 例如:

public abstract class A
{
    // Class members here.
}

抽象类不能实例化。 抽象类的用途是提供一个可供多个派生类共享的通用基类定义。

例如,类库可以定义一个抽象类,将其用作多个类库函数的参数,并要求使用该库的程序员通过创建派生类来提供自己的类实现。


抽象类可以定义字段,属性等,也可以定义抽象方法(非抽象也可定义)。 方法是将关键字 abstract 添加到方法的返回类型的前面。 例如:

public abstract class A
{
    public abstract void DoWork(int i);
}

抽象方法没有实现,所以方法定义后面是分号,而不是常规的方法块。 抽象类的派生类必须实现所有抽象方法。 当抽象类从基类继承虚方法时,抽象类可以使用抽象方法重写该虚方法。 例如:

// compile with: -target:library
public class D
{
    public virtual void DoWork(int i)
    {
        // Original implementation.
    }
}

public abstract class E : D
{
    public abstract override void DoWork(int i);
}

public class F : E
{
    public override void DoWork(int i)
    {
        // New implementation.
    }
}

如果将 virtual 方法声明为 abstract,则该方法对于从抽象类继承的所有类而言仍然是虚方法。 继承抽象方法的类无法访问方法的原始实现,因此在上一示例中,类 F 上的 DoWork 无法调用类 D 上的 DoWork。通过这种方式,抽象类可强制派生类向虚拟方法提供新的方法实现。

7.2 密封类和类成员

通过在类定义前面放置关键字 sealed,可以将类声明为密封类。 例如:

public sealed class D
{
    // Class members here.
}

密封类不能用作基类。 因此,它也不能是抽象类。 密封类禁止派生。 由于密封类从不用作基类,所以有些运行时优化可以略微提高密封类成员的调用速度。


在对基类的虚成员进行重写的派生类上,方法、索引器、属性或事件可以将该成员声明为密封成员。 在用于以后的派生类时,这将取消成员的虚效果。 方法是在类成员声明中将 sealed 关键字置于 override 关键字前面。 例如:

public class D : C
{
    public sealed override void DoWork() { }
}

8.静态类和静态类成员

8.1 静态类

静态类基本上与非静态类相同,但存在一个差异:静态类无法实例化。 换句话说,无法使用 new 关键字创建类类型的变量。 由于不存在任何实例变量,因此可以使用类名本身访问静态类的成员。

例如,如果你具有一个静态类,该类名为 UtilityClass,并且具有一个名为 MethodA 的公共方法,如下面的示例所示:

UtilityClass.MethodA();  

静态类可以用作只对输入参数进行操作并且不必获取或设置任何内部实例字段的方法集的方便容器。 例如,在 .NET Framework 类库中,静态 System.Math 类包含执行数学运算,而无需存储或检索对 Math 类特定实例唯一的数据的方法。 即,通过指定类名和方法名称来应用类的成员,如下面的示例所示。

double dub = -3.14;  
Console.WriteLine(Math.Abs(dub));  
Console.WriteLine(Math.Floor(dub));  
Console.WriteLine(Math.Round(Math.Abs(dub)));  

// Output:  
// 3.14  
// -4  
// 3  

与所有类类型的情况一样,静态类的类型信息在引用该类的程序加载时,由 .NET Framework 公共语言运行时 (CLR) 加载。 程序无法确切指定类加载的时间。

但是,可保证进行加载,以及在程序中首次引用类之前初始化其字段并调用其静态构造函数。 静态构造函数只调用一次,在程序所驻留的应用程序域的生存期内,静态类会保留在内存中。


以下列表提供静态类的主要功能:

  • 只包含静态成员。
  • 无法进行实例化。
  • 会进行密封。
  • 不能包含实例构造函数。

因此,创建静态类基本上与创建只包含静态成员和私有构造函数的类相同。 私有构造函数可防止类进行实例化。 使用静态类的优点是编译器可以进行检查,以确保不会意外地添加任何实例成员。 编译器可保证无法创建此类的实例。


静态类会进行密封,因此被不能继承。 它们也不能继承自任何类(除了 Object)。 静态类不能包含实例构造函数;但是,它们可以包含静态构造函数。 如果类包含需要进行重要初始化的静态成员,则非静态类还应定义静态构造函数。


下面是静态类的示例,该类包含将温度从摄氏度从华氏度以及从华氏度转换为摄氏度的两个方法:

public static class TemperatureConverter
{
    public static double CelsiusToFahrenheit(string temperatureCelsius)
    {
        // Convert argument to double for calculations.
        double celsius = Double.Parse(temperatureCelsius);

        // Convert Celsius to Fahrenheit.
        double fahrenheit = (celsius * 9 / 5) + 32;

        return fahrenheit;
    }

    public static double FahrenheitToCelsius(string temperatureFahrenheit)
    {
        // Convert argument to double for calculations.
        double fahrenheit = Double.Parse(temperatureFahrenheit);

        // Convert Fahrenheit to Celsius.
        double celsius = (fahrenheit - 32) * 5 / 9;

        return celsius;
    }
}

class TestTemperatureConverter
{
    static void Main()
    {
        Console.WriteLine("Please select the convertor direction");
        Console.WriteLine("1. From Celsius to Fahrenheit.");
        Console.WriteLine("2. From Fahrenheit to Celsius.");
        Console.Write(":");

        string selection = Console.ReadLine();
        double F, C = 0;

        switch (selection)
        {
            case "1":
                Console.Write("Please enter the Celsius temperature: ");
                F = TemperatureConverter.CelsiusToFahrenheit(Console.ReadLine());
                Console.WriteLine("Temperature in Fahrenheit: {0:F2}", F);
                break;

            case "2":
                Console.Write("Please enter the Fahrenheit temperature: ");
                C = TemperatureConverter.FahrenheitToCelsius(Console.ReadLine());
                Console.WriteLine("Temperature in Celsius: {0:F2}", C);
                break;

            default:
                Console.WriteLine("Please select a convertor.");
                break;
        }

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
/* Example Output:
    Please select the convertor direction
    1. From Celsius to Fahrenheit.
    2. From Fahrenheit to Celsius.
    :2
    Please enter the Fahrenheit temperature: 20
    Temperature in Celsius: -6.67
    Press any key to exit.
*/

8.2 静态成员

非静态类可以包含静态方法、字段、属性或事件。 即使未创建类的任何实例,也可对类调用静态成员。 静态成员始终按类名(而不是实例名称)进行访问。

静态成员只有一个副本存在(与创建的类的实例数无关)。 静态方法和属性无法在其包含类型中访问非静态字段和事件,它们无法访问任何对象的实例变量,除非在方法参数中显式传递它。


静态方法可以进行重载,但不能进行替代,因为它们属于类,而不属于类的任何实例。


虽然字段不能声明为 static const,不过 const 字段在其行为方面本质上是静态的。 它属于类型,而不属于类型的实例。 因此,可以使用用于静态字段的相同 ClassName.MemberName 表示法来访问常量字段。 无需进行对象实例化。


C# 不支持静态局部变量(在方法范围中声明的变量)。


可在成员的返回类型之前使用 static 关键字声明静态类成员,如下面的示例所示:

public class Automobile
{
    public static int NumberOfWheels = 4;
    public static int SizeOfGasTank
    {
        get
        {
            return 15;
        }
    }
    public static void Drive() { }
    public static event EventType RunOutOfGas;

    // Other non-static fields and properties...
}

在首次访问静态成员之前以及在调用构造函数(如果有)之前,会初始化静态成员。 若要访问静态类成员,请使用类的名称(而不是变量名称)指定成员的位置,如下面的示例所示:

Automobile.Drive();
int i = Automobile.NumberOfWheels;

如果类包含静态字段,则提供在类加载时初始化它们的静态构造函数。


对静态方法的调用会采用 Microsoft 中间语言 (MSIL) 生成调用指令,而对实例方法的调用会生成 callvirt 指令,该指令还会检查是否存在 null 对象引用。 但是在大多数时候,两者之间的性能差异并不显著。


9.访问修饰符

访问修饰符 权限
public 同一程序集中的任何其他代码或引用该程序集的其他程序集都可以访问该类型或成员。
private 只有同一类或结构中的代码可以访问该类型或成员。
protected 只有同一类或者从该类派生的类中的代码可以访问该类型或成员。
internal 同一程序集中的任何代码都可以访问该类型或成员,但其他程序集中的代码不可以。
protected internal 该类型或成员可由对其进行声明的程序集中的成员或另一程序集中的派生类中的任何代码访问。
private protected 只有在其声明程序集内,通过相同类中的代码或派生自该类的类型,才能访问类型或成员。

10.字段

可以将字段标记为public、private、protected、internal、protected internal 或 private protected。 这些访问修饰符定义该类的用户访问该字段的方式。 有关详细信息,请参阅访问修饰符。


可以选择性地将字段声明为静态。 这可使字段可供调用方在任何时候进行调用,即使不存在任何类的实例。


可以将字段声明为只读。 只能在初始化期间或在构造函数中为只读字段赋值。 static readonly 字段非常类似于常量,只不过 C# 编译器在编译时不具有对静态只读字段的值的访问权限,而只有在运行时才具有访问权限。


11.常量

常量是不可变的值,在编译时是已知的,在程序的生命周期内不会改变。 常量使用 const 修饰符声明。 仅 C# 内置类型(不包括 System.Object)可声明为 const。用户定义的类型(包括类、结构和数组)不能为 const。 使用 readonly 修饰符创建在运行时一次性(例如在构造函数中)初始化的类、结构或数组,此后不能更改。


C# 不支持 const 方法、属性或事件。


枚举类型使你能够为整数内置类型定义命名常量(例如 int、uint、long 等)。


常量在声明时必须初始化。 例如:

class Calendar1
{
    public const int months = 12;
}

在此示例中,常量 months 始终为 12,即使类本身也无法更改它。 实际上,当编译器遇到 C# 源代码中的常量标识符(例如,months)时,它直接将文本值替换到它生成的中间语言 (IL) 代码中。 因为运行时没有与常量相关联的变量地址,所以 const 字段不能通过引用传递,并且不能在表达式中显示为左值。


可以同时声明多个同一类型的常量,例如:

class Calendar2
{
    const int months = 12, weeks = 52, days = 365;
}

如果不创建循环引用,则用于初始化常量的表达式可以引用另一个常量。 例如:

class Calendar3
{
    const int months = 12;
    const int weeks = 52;
    const int days = 365;

    const double daysPerWeek = (double) days / (double) weeks;
    const double daysPerMonth = (double) days / (double) months;
}

可以将常量标记为public、private、protected、internal、protected internal 或 private protected。 这些访问修饰符定义该类的用户访问该常量的方式。


常量是作为静态字段访问的,因为常量的值对于该类型的所有实例都是相同的。 不使用 static 关键字来声明这些常量。 不在定义常量的类中的表达式必须使用类名、句点和常量名称来访问该常量。 例如:

int birthstones = Calendar.months;

若要定义整型类型(int、byte 等)的常量值,请使用枚举类型。


若要定义非整型常量,一种方法是将它们分组到一个名为 Constants 的静态类。 这要求对常量的所有引用都在其前面加上该类名,如下例所示。

static class Constants
{
    public const double Pi = 3.14159;
    public const int SpeedOfLight = 300000; // km per sec.

}
class Program
{
    static void Main()
    {
        double radius = 5.3;
        double area = Constants.Pi * (radius * radius);
        int secsFromSun = 149476000 / Constants.SpeedOfLight; // in km
    }
}

12.属性

属性是一种成员,它提供灵活的机制来读取、写入或计算私有字段的值。 属性可用作公共数据成员,但它们实际上是称为访问器的特殊方法。 这使得可以轻松访问数据,还有助于提高方法的安全性和灵活性。

12.1 属性概述

  • 属性允许类公开获取和设置值的公共方法,而隐藏实现或验证代码。
  • get 属性访问器用于返回属性值,而 set 属性访问器用于分配新值。 这些访问器可以具有不同的访问级别。 有关详细信息,请参阅限制访问器可访问性。
  • value 关键字用于定义由 set 访问器分配的值。
  • 属性可以是读-写属性(既有 get 访问器又有 set 访问器)、只读属性(有 get 访问器,但没有 set 访问器)或只写访问器(有 set 访问器,但没有 get 访问器)。 只写属性很少出现,常用于限制对敏感数据的访问。
  • 不需要自定义访问器代码的简单属性可以作为表达式主体定义或自动实现的属性来实现。

12.2 具有支持字段的属性

有一个实现属性的基本模式,该模式使用私有支持字段来设置和检索属性值。 get 访问器返回私有字段的值,set 访问器在向私有字段赋值之前可能会执行一些数据验证。 这两个访问器还可以在存储或返回数据之前对其执行某些转换或计算。


下面的示例阐释了此模式。 在此示例中,TimePeriod 类表示时间间隔。 在内部,该类将时间间隔以秒为单位存储在名为 seconds 的私有字段中。 名为 Hours 的读-写属性允许客户以小时为单位指定时间间隔。 get 和 set 访问器都会执行小时与秒之间的必要转换。 此外,set 访问器还会验证数据,如果小时数无效,则引发 ArgumentOutOfRangeException。

using System;

class TimePeriod
{
   private double seconds;

   public double Hours
   {
       get { return seconds / 3600; }
       set { 
          if (value < 0 || value > 24)
             throw new ArgumentOutOfRangeException(
                   $"{nameof(value)} must be between 0 and 24.");

          seconds = value * 3600; 
       }
   }
}

class Program
{
   static void Main()
   {
       TimePeriod t = new TimePeriod();
       // The property assignment causes the 'set' accessor to be called.
       t.Hours = 24;

       // Retrieving the property causes the 'get' accessor to be called.
       Console.WriteLine($"Time in hours: {t.Hours}");
   }
}
// The example displays the following output:
//    Time in hours: 24

12.3 表达式主体定义

属性访问器通常由单行语句组成,这些语句只分配或只返回表达式的结果。 可以将这些属性作为 expression-bodied 成员来实现。 => 符号后跟用于为属性赋值或从属性中检索值的表达式,即组成了表达式主体定义。


从 C# 6 开始,只读属性可以将 get 访问器作为 expression-bodied 成员实现。 在这种情况下,既不使用 get 访问器关键字,也不使用 return 关键字。 下面的示例将只读 Name 属性作为 expression-bodied 成员实现。

public string Name => $"{firstName} {lastName}";   

从 C# 7.0 开始,get 和 set 访问器都可以作为 expression-bodied 成员实现。 在这种情况下,必须使用 get 和 set 关键字。 下面的示例阐释如何为这两个访问器使用表达式主体定义。 请注意,return 关键字不与 get 访问器搭配使用。

using System;

public class SaleItem
{
   string name;
   decimal cost;
   
   public SaleItem(string name, decimal cost)
   {
      this.name = name;
      this.cost = cost;
   }

   public string Name 
   {
      get => name;
      set => name = value;
   }

   public decimal Price
   {
      get => cost;
      set => cost = value; 
   }
}

class Program
{
   static void Main(string[] args)
   {
      var item = new SaleItem("Shoes", 19.95m);
      Console.WriteLine($"{item.Name}: sells for {item.Price:C2}");
   }
}
// The example displays output like the following:
//       Shoes: sells for $19.95

12.4 自动实现的属性

在某些情况下,属性 get 和 set 访问器仅向支持字段赋值或仅从其中检索值,而不包括任何附加逻辑。 通过使用自动实现的属性,既能简化代码,还能让 C# 编译器透明地提供支持字段。


如果属性具有 get 和 set 访问器,则必须自动实现这两个访问器。 自动实现的属性通过以下方式定义:使用 get 和 set 关键字,但不提供任何实现。 下面的示例与上一个示例基本相同,只不过 Name 和 Price 是自动实现的属性。 请注意,该示例还删除了参数化构造函数,以便通过调用默认构造函数和对象初始值设定项立即初始化 SaleItem 对象。

using System;

public class SaleItem
{
   public string Name 
   { get; set; }

   public decimal Price
   { get; set; }
}

class Program
{
   static void Main(string[] args)
   {
      var item = new SaleItem{ Name = "Shoes", Price = 19.95m };
      Console.WriteLine($"{item.Name}: sells for {item.Price:C2}");
   }
}
// The example displays output like the following:
//       Shoes: sells for $19.95

12.5 接口属性

可以在接口上声明属性。 下面是接口属性访问器的示例:

public interface ISampleInterface
{
    // Property declaration:
    string Name
    {
        get;
        set;
    }
}

接口属性的访问器没有正文。 因此,访问器的用途是指示属性为读写、只读还是只写。

12.6 限制访问器可访问性

属性或索引器的 get 和 set 部分称为访问器。 默认情况下,这些访问器具有相同的可见性或访问级别:其所属属性或索引器的可见性或访问级别。 有关详细信息,请参阅可访问性级别。 不过,有时限制对其中某个访问器的访问是有益的。 通常是在保持 get 访问器可公开访问的情况下,限制 set 访问器的可访问性。 例如:

private string name = "Hello";

public string Name
{
    get
    {
        return name;
    }
    protected set
    {
        name = value;
    }
}

在此示例中,名为 Name 的属性定义 get 访问器和 set 访问器。 get 访问器接收该属性本身的可访问性级别(此示例中为 public),而对于 set 访问器,则通过对该访问器本身应用 protected 访问修饰符来进行显式限制。


对访问器的访问修饰符的限制,对属性或索引器使用访问修饰符受以下条件的制约:

  • 不能对接口或显式接口成员实现使用访问器修饰符。
  • 仅当属性或索引器同时具有 set 和 get 访问器时,才能使用访问器修饰符。 这种情况下,只允许对其中一个访问器使用修饰符。
  • 如果属性或索引器具有 override 修饰符,则访问器修饰符必须与重写的访问器的访问器(如有)匹配。
  • 访问器的可访问性级别必须比属性或索引器本身的可访问性级别具有更严格的限制。

13.方法

13.1 本地函数

本地函数是一种嵌套在另一成员中的类型的私有方法。 仅能从其包含成员中调用它们。


与方法定义不同,本地函数定义不能包含下列元素:

  • 成员访问修饰符。 因为所有本地函数都是私有的,使用任何访问修饰符(包括 private 关键字)都是非法的。
  • Static 关键字。

此外,属性不能应用于本地函数或其参数和类型参数。


以下示例定义了一个名为 AppendPathSeparator 的本地函数,该函数对于名为 GetText 的方法是私有的:

using System;
using System.IO;

class Example
{
    static void Main()
    {
        string contents = GetText(@"C:	emp", "example.txt");
        Console.WriteLine("Contents of the file:
" + contents);
    }
   
    private static string GetText(string path, string filename)
    {
         var sr = File.OpenText(AppendPathSeparator(path) + filename);
         var text = sr.ReadToEnd();
         return text;
         
         // Declare a local function.
         string AppendPathSeparator(string filepath)
         {
            if (! filepath.EndsWith(@""))
               filepath += @"";

            return filepath;   
         }
    } 
}

13.2 ref 返回值和局部变量

13.2.1 什么是引用返回值

如果声明方法返回引用返回值,表明方法返回变量别名。 这样做通常是为了让调用代码有权通过别名访问此变量(包括修改它)。 因此,方法的引用返回值不得包含返回类型 void。


对于方法能以引用返回值的形式返回的表达式,存在一些限制。 具体限制包括:

  • 返回值的生存期必须长于方法执行时间。
  • 返回值不得为文本 null。
    使用引用返回值的方法可以返回值当前为 null(未实例化)或可以为 null 的类型的变量别名。
  • 返回值不得为常量、枚举成员、通过属性的按值返回值或 class/struct 方法。

此外,禁止对异步方法使用引用返回值。 异步方法可能会在执行尚未完成时就返回值,尽管返回值仍未知。

13.2.2 定义 ref 返回值

返回引用返回值的方法必须满足以下两个条件:

  • 方法签名在返回类型前面有 ref 关键字。
  • 方法主体中的每个 return 语句都在返回实例的名称前面有 ref 关键字。

下面的示例方法满足这些条件,且返回对名为 p 的 Person 对象的引用:

public ref Person GetContactInformation(string fname, string lname)
{
    // ...method implementation...
    return ref p;
}
13.2.3 使用 ref 返回值

引用返回值是被调用方法范围中另一个变量的别名。 可以将引用返回值的所有使用都解释为,使用它取别名的变量:

  • 分配值时,就是将值分配到它取别名的变量。
  • 读取值时,就是读取它取别名的变量的值。
  • 如果以引用方式返回它,就是返回对相同变量所取的别名。
  • 如果以引用方式将它传递到另一个方法,就是传递对它取别名的变量的引用。
  • 如果返回引用本地别名,就是返回相同变量的新别名。

假设 GetContactInformation 方法声明为引用返回:

public ref Person GetContactInformation(string fname, string lname)

按值分配会读取变量值,并将它分配给新变量:

Person p = contacts.GetContactInformation("Brandie", "Best");
13.2.4 ref 返回结果和 ref 局部变量:示例

下列示例定义存储整数值数组的 NumberStore 类。 FindNumber 方法按引用返回第一个大于或等于作为参数传递的数字的数字。 如果没有大于或等于该参数的数字,则方法返回索引 0 中的数字。

using System;

class NumberStore
{
    int[] numbers = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023 };

    public ref int FindNumber(int target)
    {
        for (int ctr = 0; ctr < numbers.Length; ctr++)
        {
            if (numbers[ctr] >= target)
                return ref numbers[ctr];
        }
        return ref numbers[0];
    }

    public override string ToString() => string.Join(" ", numbers);
}

下列示例调用 NumberStore.FindNumber 方法来检索大于或等于 16 的第一个值。 然后,调用方将该方法返回的值加倍。 示例输出表明,NumberStore 实例的数组元素值反映了更改。

var store = new NumberStore();
Console.WriteLine($"Original sequence: {store.ToString()}");
int number = 16;
ref var value = ref store.FindNumber(number);
value *= 2;
Console.WriteLine($"New sequence:      {store.ToString()}");
// The example displays the following output:
//       Original sequence: 1 3 7 15 31 63 127 255 511 1023
//       New sequence:      1 3 7 15 62 63 127 255 511 1023

如果引用返回值不受支持,需要通过返回数组元素及其值的索引来执行此类操作。 然后,调用方可使用此索引修改单个方法调用中的值。 但调用方也可修改要访问的索引,还可修改其他数组值。


下面的示例展示了如何在高于 C# 7.3 的版本中将 FindNumber 方法重写为使用 ref 局部重新分配:

using System;

class NumberStore
{
    int[] numbers = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023 };

    public ref int FindNumber(int target)
    {
        ref int returnVal = ref numbers[0];
        var ctr = numbers.Length - 1;
        while ((ctr > 0) && numbers[ctr] >= target)
        {
            returnVal = ref numbers[ctr];
            ctr--;
        }
        return ref returnVal;
    }

    public override string ToString() => string.Join(" ", numbers);
}

如果查找的数字更接近数组末尾,这第二个版本就更高效且序列更长。

13.3 传递参数

一个值类型变量包含它直接与引用类型变量相对的数据,其中包含对其数据的引用。 按值将值-类型变量传递给方法,意味着将变量副本传递给该方法。 在方法内发生的对该实参进行的任何更改不会影响存储在形参变量中的原始数据。

// 按值传递
class Project
{
    public static void Swap(int a, int b)
    {
        int temp = a;
        a = b;
        b = temp;
    }
    static void Main()
    {
        int a = 0;
        int b = 1;
        Swap(a, b);
        Console.WriteLine($"a = {a}, b = {b}");
        Console.ReadKey();
    }
}

output:
a = 0, b = 1

如果想用调用的方法来更改参数的值,必须使用 ref 或 out 关键字,通过引用进行传递。 还可以使用 in 关键字来按引用传递值参数,以避免复制并同时保证不更改值。

// ref
class Project
{
    public static void Swap(ref int a, ref int b)
    {
        int temp = a;
        a = b;
        b = temp;
    }
    static void Main()
    {
        int a = 0;
        int b = 1;
        Swap(ref a, ref b);
        Console.WriteLine($"a = {a}, b = {b}");
        Console.ReadKey();
    }
}

output:
a = 1, b = 0
// out
class Project
{
    public static void Method(out int a)
    {
        int b = 10;
        a = b * 10;
    }
    static void Main()
    {
        int a;
        Method(out a);
        Console.WriteLine($"a = {a}");
        Console.ReadKey();
    }
}

output:
a = 100

13.4 隐式类型的局部变量

可声明局部变量而无需提供显式类型。 var 关键字指示编译器通过初始化语句右侧的表达式推断变量的类型。 推断类型可以是内置类型、匿名类型、用户定义类型或 .NET Framework 类库中定义的类型。 有关如何使用 var 初始化数组的详细信息,请参阅隐式类型化数组。


以下示例演示使用 var 声明局部变量的各种方式:

// i is compiled as an int
var i = 5;

// s is compiled as a string
var s = "Hello";

// a is compiled as int[]
var a = new[] { 0, 1, 2 };

// expr is compiled as IEnumerable<Customer>
// or perhaps IQueryable<Customer>
var expr =
    from c in customers
    where c.City == "London"
    select c;

// anon is compiled as an anonymous type
var anon = new { Name = "Terry", Age = 34 };

// list is compiled as List<int>                             
var list = new List<int>();

重要的是了解 var 关键字并不意味着“变体”,并且并不指示变量是松散类型或是后期绑定。 它只表示由编译器确定并分配最适合的类型。


在以下上下文中,可使用 var 关键字:

  • 在局部变量(在方法范围内声明的变量)上,如前面的示例所示。

  • 在 for 初始化语句中。
for(var x = 1; x < 10; x++)  
  • 在 foreach 初始化语句中。
foreach(var item in list){...}  
  • 在 using 域间中。
using (var file = new StreamReader("C:\myfile.txt")) {...}  

var 和匿名类型
在许多情况下,使用 var 是可选的,只是一种语法便利。 但是,在使用匿名类型初始化变量时,如果需要在以后访问对象的属性,则必须将变量声明为 var。 这是 LINQ 查询表达式中的常见方案。


从源代码角度来看,匿名类型没有名称。 因此,如果使用 var 初始化了查询变量,则访问返回对象序列中的属性的唯一方法是在 foreach 语句中将 var 用作迭代变量的类型。

class ImplicitlyTypedLocals2
{
    static void Main()
    {
        string[] words = { "aPPLE", "BlUeBeRrY", "cHeRry" };

        // If a query produces a sequence of anonymous types, 
        // then use var in the foreach statement to access the properties.
        var upperLowerWords =
             from w in words
             select new { Upper = w.ToUpper(), Lower = w.ToLower() };

        // Execute the query
        foreach (var ul in upperLowerWords)
        {
            Console.WriteLine("Uppercase: {0}, Lowercase: {1}", ul.Upper, ul.Lower);
        }
    }
}
/* Outputs:
    Uppercase: APPLE, Lowercase: apple
    Uppercase: BLUEBERRY, Lowercase: blueberry
    Uppercase: CHERRY, Lowercase: cherry        
*/

13.5 扩展方法

扩展方法可用于为一个类型增加行为


定义和调用扩展方法:

  • 定义包含扩展方法的静态类。
  • 此类必须对客户端代码可见(可访问性)。
  • 将扩展方法实现为静态方法,并且使其可见性至少与所在类的可见性相同。
  • 此方法的第一个参数指定方法所操作的类型;此参数前面必须加上 this 修饰符。
  • 在调用代码中,添加 using 指令,用于指定包含扩展方法类的命名空间。
  • 和调用类型的实例方法那样调用这些方法。
namespace N1
{
    public static class StringExtension
    {
        public static void AddOne(this int a)
        {
            a++;
            Console.WriteLine($"num = {a}");
        }
    }
}
namespace N2
{
    using N1;
    class Program
    {
        static void Main(string[] args)
        {
            int num = 0;
            num.AddOne();
            Console.ReadKey();
        }
    }
}

output:
num = 1

示例:为枚举创建新方法

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;

namespace EnumExtension
{
    // Define an extension method in a non-nested static class.
    public static class Extensions
    {        
        public static Grades minPassing = Grades.D;
        public static bool Passing(this Grades grade)
        {
            return grade >= minPassing;
        }
    }

    public enum Grades { F = 0, D=1, C=2, B=3, A=4 };
    class Program
    {       
        static void Main(string[] args)
        {
            Grades g1 = Grades.D;
            Grades g2 = Grades.F;
            Console.WriteLine("First {0} a passing grade.", g1.Passing() ? "is" : "is not");
            Console.WriteLine("Second {0} a passing grade.", g2.Passing() ? "is" : "is not");

            Extensions.minPassing = Grades.C;
            Console.WriteLine("First {0} a passing grade.", g1.Passing() ? "is" : "is not");
            Console.WriteLine("Second {0} a passing grade.", g2.Passing() ? "is" : "is not");
        }
    }
  }
/* Output:
    First is a passing grade.
    Second is not a passing grade.

    First is not a passing grade.
    Second is not a passing grade.
 */

14.构造函数

14.1 实例构造函数

使用 new 表达式创建类的对象时,实例构造函数可用于创建和初始化任意实例成员变量。 若要初始化静态类或非静态类中的静态变量,必须定义静态构造函数。


下面的示例演示了实例构造函数:

class CoOrds
{
    public int x, y;

    // constructor
    public CoOrds()
    {
        x = 0;
        y = 0;
    }
}

只要创建基于 CoOrds 类的对象,就会调用此实例构造函数。 诸如此类不带参数的构造函数称为“默认构造函数”。 然而,提供其他构造函数通常十分有用。 例如,可以将构造函数添加到 CoOrds 类,以便可以为数据成员指定初始值:

// A constructor with two arguments:
public CoOrds(int x, int y)
{
    this.x = x;
    this.y = y;
}

这样便可以用默认或特定的初始值创建 CoOrd 对象,如下所示:

CoOrds p1 = new CoOrds();
CoOrds p2 = new CoOrds(5, 3);

如果某个类没有构造函数,则会自动生成一个默认构造函数,并使用默认值来初始化对象字段。 例如,int 初始化为 0。 由于 CoOrds 类的默认构造函数将所有数据成员都初始化为零,因此可以将它完全移除,而不会更改类的工作方式。


也可以用实例构造函数来调用基类的实例构造函数。 类构造函数可通过初始值设定项来调用基类的构造函数,如下所示:

class Circle : Shape
{
    public Circle(double radius)
        : base(radius, 0)
    {
    }
}

在此示例中,Circle 类将半径和高度的值传递给 Shape(Circle 从它派生而来)提供的构造函数。

14.2 私有构造函数

私有构造函数是一种特殊的实例构造函数。 它通常用于只包含静态成员的类中。 如果类具有一个或多个私有构造函数而没有公共构造函数,则其他类(除嵌套类外)无法创建该类的实例。 例如:

class NLog
{
    // Private Constructor:
    private NLog() { }

    public static double e = Math.E;  //2.71828...
}

14.3 静态构造函数

静态构造函数用于初始化任何静态数据,或执行仅需执行一次的特定操作。 将在创建第一个实例或引用任何静态成员之前自动调用静态构造函数。

class SimpleClass
{
    // Static variable that must be initialized at run time.
    static readonly long baseline;

    // Static constructor is called at most one time, before any
    // instance constructor is invoked or member is accessed.
    static SimpleClass()
    {
        baseline = DateTime.Now.Ticks;
    }
}

静态构造函数具有以下属性:

  • 静态构造函数不使用访问修饰符或不具有参数。
  • 在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数以初始化类。
  • 不能直接调用静态构造函数。
  • 用户无法控制在程序中执行静态构造函数的时间。
  • 静态构造函数的一种典型用法是在类使用日志文件且将构造函数用于将条目写入到此文件中时使用。
  • 静态构造函数对于创建非托管代码的包装类也非常有用,这种情况下构造函数可调用 LoadLibrary 方法。
  • 如果静态构造函数引发异常,运行时将不会再次调用该函数,并且类型在程序运行所在的应用程序域的生存期内将保持未初始化。

在此示例中,类 Bus 具有静态构造函数。 创建 Bus 的第一个实例 (bus1) 时,将调用该静态构造函数,以便初始化类。 示例输出验证即使创建了两个 Bus 的实例,静态构造函数也仅运行一次,并且在实例构造函数运行前运行。

 public class Bus
 {
     protected static readonly DateTime globalStartTime;

     protected int RouteNumber { get; set; }

     static Bus()
     {
         globalStartTime = DateTime.Now;

         Console.WriteLine("Static constructor sets global start time to {0}",
             globalStartTime.ToLongTimeString());
     }

     public Bus(int routeNum)
     {
         RouteNumber = routeNum;
         Console.WriteLine("Bus #{0} is created.", RouteNumber);
     }

     public void Drive()
     {
         TimeSpan elapsedTime = DateTime.Now - globalStartTime;

         Console.WriteLine("{0} is starting its route {1:N2} minutes after global start time {2}.",
                                 this.RouteNumber,
                                 elapsedTime.TotalMilliseconds,
                                 globalStartTime.ToShortTimeString());
     }
 }

 class TestBus
 {
     static void Main()
     {
         Bus bus1 = new Bus(71);

         Bus bus2 = new Bus(72);

         bus1.Drive();

         System.Threading.Thread.Sleep(25);

         bus2.Drive();

         System.Console.WriteLine("Press any key to exit.");
         System.Console.ReadKey();
     }
 }
output:
Static constructor sets global start time to 3:57:08 PM.
Bus #71 is created.
Bus #72 is created.
71 is starting its route 6.00 minutes after global start time 3:57 PM.
72 is starting its route 31.00 minutes after global start time 3:57 PM.      

14.4 编写复制构造函数

public class People
{
    public string Name { get; set; }
    public int Age { get; set; }
    public People(People person)
    {
        Name = person.Name;
        Age = person.Age;
    }
    public People(string name, int age)
    {
        Name = name;
        Age = age;
    }
}
public class Program
{
    static void Main()
    {
        People p1 = new People("kyle", 23);
        People p2 = new People(p1);
        Console.WriteLine($"name:{p1.Name}, age:{p1.Age}");
        Console.WriteLine($"name:{p2.Name}, age:{p2.Age}");
        Console.ReadKey();
    }
}

output:
name:kyle, age:23
name:kyle, age:23

15.终结器

终结器用于析构类的实例。

  • 无法在结构中定义终结器。 它们仅用于类。
  • 一个类只能有一个终结器。
  • 不能继承或重载终结器。
  • 不能手动调用终结器。 可以自动调用它们。
  • 终结器不使用修饰符或参数。

例如,以下是类 Car 的终结器声明。

class Car
{
    ~Car()  // destructor
    {
        // cleanup statements...
    }
}

终结器也可以作为表达式主体定义实现,如下面的示例所示。

using System;

public class Destroyer
{
   public override string ToString() => GetType().Name;
   
   ~Destroyer() => Console.WriteLine($"The {ToString()} destructor is executing.");
}

终结器隐式调用对象基类上的 Finalize。 因此,对终结器的调用会隐式转换为以下代码:

protected override void Finalize()  
{  
    try  
    {  
        // Cleanup statements...  
    }  
    finally  
    {  
        base.Finalize();  
    }  
}  

这意味着,对继承链(从派生程度最高到派生程度最低)中的所有实例以递归方式调用 Finalize 方法。


16.对象和集合初始值设定项

使用对象初始值设定项,你可以在创建对象时向对象的任何可访问字段或属性分配值,而无需调用后跟赋值语句行的构造函数。

利用对象初始值设定项语法,你可为构造函数指定参数或忽略参数(以及括号语法)。 以下示例演示如何使用具有命名类型 Cat 的对象初始值设定项以及如何调用默认构造函数。

class Cat
{
    // Auto-implemented properties.
    public int Age { get; set; }
    public string Name { get; set; }
}
Cat cat = new Cat { Age = 10, Name = "Fluffy" };

对象初始值设定项语法允许你创建一个实例,然后将具有其分配属性的新建对象指定给赋值中的变量。

16.1 具有匿名类型的对象初始值设定项

尽管对象初始值设定项可用于任何上下文中,但它们在 LINQ 查询表达式中特别有用。 查询表达式常使用只能通过使用对象初始值设定项进行初始化的匿名类型,如下面的声明所示。

var pet = new { Age = 10, Name = "Fluffy" };  

利用匿名类型,LINQ 查询表达式中的 select 子句可以将原始序列的对象转换为其值和形状可能不同于原始序列的对象。 如果你只想存储某个序列中每个对象的部分信息,则这很有用。 在下面的示例中,假定产品对象 (p) 包含很多字段和方法,而你只想创建包含产品名和单价的对象序列。

var productInfos =
    from p in products
    select new { p.ProductName, p.UnitPrice };

执行此查询时,productInfos 变量将包含一系列对象,这些对象可以在 foreach 语句中进行访问,如下面的示例所示:

foreach(var p in productInfos){...}  

新的匿名类型中的每个对象都具有两个公共属性,这两个属性接收与原始对象中的属性或字段相同的名称。 你还可在创建匿名类型时重命名字段;下面的示例将 UnitPrice 字段重命名为 Price。

select new {p.ProductName, Price = p.UnitPrice};  

16.2 集合初始值设定项

在初始化实现 IEnumerable 的集合类型和初始化使用适当的签名作为实例方法或扩展方法的 Add 时,集合初始值设定项允许指定一个或多个元素初始值设定项。

元素初始值设定项可以是简单的值、表达式或对象初始值设定项。 通过使用集合初始值设定项,你将无需在源代码中指定对该类的 Add 方法的多个调用;编译器将添加这些调用。


下面的示例演示了两个简单的集合初始值设定项:

List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };  
List<int> digits2 = new List<int> { 0 + 1, 12 % 3, MakeInt() };  

下面的集合初始值设定项使用对象初始值设定项来初始化上一个示例中定义的 Cat 类的对象。 请注意,各个对象初始值设定项分别括在大括号中且用逗号隔开。

List<Cat> cats = new List<Cat>
{
    new Cat(){ Name = "Sylvester", Age=8 },
    new Cat(){ Name = "Whiskers", Age=2 },
    new Cat(){ Name = "Sasha", Age=14 }
};

如果集合的 Add 方法允许,则可以将 null 指定为集合初始值设定项中的一个元素。

List<Cat> moreCats = new List<Cat>
{
    new Cat(){ Name = "Furrytail", Age=5 },
    new Cat(){ Name = "Peaches", Age=4 },
    null
};

如果集合支持索引,可以指定索引元素。

var numbers = new Dictionary<int, string> {   
    [7] = "seven",   
    [9] = "nine",   
    [13] = "thirteen"   
}; 

17.嵌套类型

在类或构造中定义的类型称为嵌套类型。 例如:

class Container
{
    class Nested
    {
        Nested() { }
    }
}

不论外部类型是类还是构造,嵌套类型均默认为 private;仅可从其包含类型中进行访问。 在上一个示例中,Nested 类无法访问外部类型。


以下示例使 Nested 类为 public:

class Container
{
    public class Nested
    {
        Nested() { }
    }
}

嵌套类型(或内部类型)可访问包含类型(或外部类型)。 若要访问包含类型,请将其作为参数传递给嵌套类型的构造函数。 例如:

public class Container
{
    public class Nested
    {
        private Container parent;

        public Nested()
        {
        }
        public Nested(Container parent)
        {
            this.parent = parent;
        }
    }
}

嵌套类型可以访问其包含类型可以访问的所有成员。 它可以访问包含类型的私有成员和受保护成员(包括所有继承的受保护成员)。


在前面的声明中,类 Nested 的完整名称为 Container.Nested。 这是用来创建嵌套类新实例的名称,如下所示:

Container.Nested nest = new Container.Nested();

18.分部类和方法

18.1 分部类

在以下几种情况下需要拆分类定义:

  • 处理大型项目时,使一个类分布于多个独立文件中可以让多位程序员同时对该类进行处理。
  • 使用自动生成的源时,无需重新创建源文件便可将代码添加到类中。 Visual Studio 在创建 Windows 窗体、Web 服务包装器代码等时都使用此方法。 无需修改 Visual Studio 创建的文件,就可创建使用这些类的代码。

若要拆分类定义,请使用 partial 关键字修饰符,如下所示:

public partial class Employee
{
    public void DoWork()
    {
    }
}

public partial class Employee
{
    public void GoToLunch()
    {
    }
}

18.2 分部方法

分部方法声明由两个部分组成:定义和实现。 它们可以位于分部类的不同部分中,也可以位于同一部分中。 如果不存在实现声明,则编译器会优化定义声明和对方法的所有调用。

// Definition in file1.cs  
partial void onNameChanged();  

// Implementation in file2.cs  
partial void onNameChanged()  
{  
  // method body  
}  

19.匿名类型

匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成,并且不能在源代码级使用。 每个属性的类型由编译器推断。


可通过使用 new 运算符和对象初始值创建匿名类型。 有关对象初始值设定项的详细信息,请参阅对象和集合初始值设定项。


以下示例显示了用两个名为 Amount 和 Message 的属性进行初始化的匿名类型。

var v = new { Amount = 108, Message = "Hello" };  
Console.WriteLine(v.Amount + v.Message);  

20.类与结构的对比

类class 结构struct
引用类型(可为null) 值类型
创建在托管堆上 创建在栈上
使用对象的引用 直接使用对象
作参时传递引用 作参时传递值
有初始化器 无初始化器
须new关键字实例化 无需new也可实力化
支持继承、多态 支持接口
全修饰符 无protected和protected internal
构造器无需初始化所有字段 构造器须初始化所有字段
有析构 无析构
大而复杂的数据 数据组合成的新类型(点、线)
  • 引用类型:所有的类,包括接口、委托;string是特殊的引用类型(只读);数组;已装箱值类型(object)

  • 值类型:内置值类型;用户自定义值类型(结构);枚举

  • 堆(Heap):地址从低到高分配,所有引用类型的对象分配在托管堆上

  • 栈(Stack):地址从高到低分配,所有的值类型及引用类型的引用分配在栈上

至目录    至页首

原文地址:https://www.cnblogs.com/jizhiqiliao/p/10648938.html