【C#】一些八股文笔记

笔记源于正在阅读的《C#高级编程》

第一章

CLR(Common Language Runtime, 公共语言运行库)是每种.NET编程语言都使用的运行库,.NET编程语言的编译器生成中间语言(Intermediate Language, IL)代码(也称托管代码),CLR包含一个即时(Just-In-Time, JIT)编译器,当程序开始运行时,JIT 编译器会从 IL 代码中生成本地代码。

NET Core 使用 CoreCLR,而 .NET Framework 使用 CLR。

 

.NET Standard 是一个规范,定义了在任何支持该标准的平台上应该使用哪些API。标准版本越高,可用的API越多。

使用.NET Core和.NET Standard构建的程序,共享相同的编译器平台、编程语言和运行库组件,它们不共享相同的运行库,但是它们在运行时共享组件。

 

.NET Framework 要求必须在系统上安装应用程序需要的特定版本,而在.NET Core 1.0 中,框架(包括运行库)是与应用程序一起交付的。

应用程序不需要在目标系统上安装运行库,而是可以用它交付运行库,这就是自包含部署。平台不同,运行库不同,因此,需要在项目文件中指定 RuntimeIdentifiers,来指定支持的平台。

 

WPF 基于 DirectX。

ASP.NET Web API 基于 Representational State Transfer(REST)软件架构风格。

SignalR 使用 WebSocket 技术推送信息。

NuGet 包是一个 Zip 文件,其中包括程序集、配置信息和 PowerShell 脚本。

 

软件即服务(Software as a Service, SaaS),如 Office365。

基础设施即服务(Infrastructure as a Service, IaaS),如虚拟机。

平台即服务(Platform as a Service, PaaS),Microsoft Azure 最相关的部分。

函数即服务(Functions as a Service, FaaS),支付金额基于消费。

 

第二章

使用 using static 声明,不仅可以打开名称空间,还可以打开类的所有静态成员。例如 using static System.Console; 之后便可以调用 WriteLine 方法而不使用类名。using 关键字还可以用于给类和名称空间指定别名,名称空间别名的修饰符是 ::(双冒号)

 

C#编译器需要用某个初始值对变量进行初始化,之后才能在操作中引用该变量。C#有两个方法可确保变量在使用前进行了初始化:

1.变量是类或结构中的字段,如果没有显示初始化,则创建这些变量时,其默认值为0。

2.方法的局部变量必须在代码中显式初始化,之后才能在语句中使用它们的值。初始化不一定在声明变量时进行,但编译器会通过方法检查所有可能的路径,如果局部变量在初始化之前就被使用,那么会被标记为错误。

 

类型推断使用 var 关键字,此时变量必须在声明时初始化,否则编译器没有推断变量类型的依据。声明变量且推断出类型后, 就无法改变变量的类型。

 

变量作用域规则:

1.只要类的局部变量在某个作用域内,其字段也在该作用域内。

2.局部变量存在域表示声明该变量的块语句或方法结束的右花括号内。

3.在 for、while 或类似语句中声明的局部变量存在于该循环体内。

同名的局部变量不能在同一作用域内声明两次,但字段和局部变量可以同名,原因是 C# 在变量之间有一个基本的区分,它把在类型级别声明的变量看成字段,而把方法中声明的变量看成局部变量,同名时会默认屏蔽类级别的变量,如果需要访问同名的类级别变量,需要使用类名(静态字段)或者 this(实例字段)。

 

常量规则:

1.常量必须在声明时初始化。

2.常量的值必须在编译时用于计算。如果需要在运行时确定其值,应使用只读字段。

3.常量总是隐式静态的,但不能使用 static 修饰符。

 

值类型储存在堆栈中,而引用类型储存在托管堆上。如果要把自己的类型定义为值类型,就应该声明为一个结构。

 

在C#中,int 是32位有符号整数,long 是64位有符号整数,short 是16位有符号整数,sbyte 是8位有符号整数,byte 是8位无符号整数,在C#中所有的数据类型都以与平台无关的方式定义。

如果在代码中对某个非整数值硬编码,则编译器一般假定它是 double,如果要指定其类型为 float,要加上后缀 F 或 f。

在C#中,bool 值和整数值不能互相隐式转换。在 if 语句中,布尔表达式是必需的(但C++允许使用整数值)。

在C#中,char 类型表示一个16位的 Unicode 字符(两字节)。

 

字符串是不可改变的,修改其中一个字符串,就会创建一个全新的 string 对象,而原来的字符串不发生任何变化。这是运算符重载的结果。

字符串字面量前面加上字符 @,就可以禁止转义字符解释,所有字符都是其原来的含义。

字符串字面量前面加上字符 $,就可以使用花括号语法进行插值。

 

编译器会把没有 break 语句的 case 字句标记为错误,除了空 case 语句除外。

switch 语句中的 case 子句的顺序是无关紧要的,任何两个 case 都不能相同。

在C#中,可以把字符串用作 switch 选择的变量,在C++中不行。

 

foreach 循环中,不能改变集合中各项的值。

 

名称空间可以嵌套,可以在名称空间的名字中用点号组织。名称空间与程序集无关,同一个程序集可以有不同的名称空间,也可以在不同的程序集中定义同一个名称空间的类型。名称空间的名称是.NET区分共享程序集中对象名的唯一方式。

 

对于类中的变量表示,在下列情况下应该用属性而不是方法:

1.客户端代码应能读取它的值(但如果只需写入值,应该使用方法而不是只读属性)。

2.读取该值不应花太长时间,且不应有任何明显的和不希望的负面效应。

4.可以按照任何顺序设置属性,顺序读取属性应有相同的结果。

 

字段一般应总是私有的,在某些情况下可以把常量和只读字段设置为公有。公有字段不利于扩展和修改类。

 

第三章

结构不同于类,因为它们不需要在堆上分配空间,类是引用类型,总是储存在堆上,结构是值类型,通常储存在栈上,结构不支持继承。对于类和结构,都使用 new 关键字声明实例。

类的构造函数必须与类名相同,且没有返回值。

 

字段可以用 readonly 修饰符声明为只读字段,与 const 修饰符不同,编译器用其值取代了 const 变量,因为编译器知道常量的值。但只读字段在运行期间确定其值,且与常量字段相反,只读字段可以是实例成员,也可以用 static 修饰符。const 成员与类相关,且不允许使用 static 修饰符。只读字段要么在构造函数中赋值,要么在声明时赋值(不赋值时取默认值)。

 

属性具有 get 和 set 访问器。get 访问器不带任何参数,且必须返回属性声明的类型。也不应为 set 访问器指定任何显式参数,但编译器假定它带一个参数,其类型与属性相同,名称为 value。最好把字段声明为 private,使用属性来访问字段。

如果不需要任何附加的逻辑,则可以使用自动实现的属性,不需要声明私有字段,编译器会自动创建。如 public int Age { get; set; }

自动实现的属性可以初始化。属性的访问器可以设置修饰符,在 get 和 set 访问器中,必须有一个具备属性的访问级别。使用自动实现的属性,也可以设置修饰符。如 public int Age { get; private set; }

在属性定义中省略 set 访问器,就可以创建只读属性。只读属性一般和只读字段配合使用,不是强制的,但自动实现的只读属性,编译器会创建一个只读字段。不要创建只写属性,应该用方法替代。

属性访问器可以配合 lambda 表达式使用,属性也可以配合 lambda 表达式使用,这时候会创建一个只读属性。如 public int Age => age;

 

如果类型包含可以改变的成员,那么它就是一个可变类型,如果对象没有任何可以改变的成员,只有只读成员,它就是一个不可变类型,其内容只能在构造函数中初始化。比如 String 类。

匿名类型通过 var 和 new 关建字使用,匿名类型是一个继承自 Object 且没有名称的类(这说明它是引用类型),该类的定义从初始化器中推断,在初始化器中,不需要声明成员类型,编译器从成员的值中推断。在所有属性匹配时,即成员数量、类型、顺序都相同时,匿名类型相同,变量之间可以相互赋值。如果所设置的值来自于另一个对象,则可以推断成员的名称,此时该对象的属性名投射到新对象当中。例如:

var a = new { a = 1, b = "str" };
var b = new { a = 2, b = "str2" };
var c = new { a.a, a.b };
Console.WriteLine($"{a.b} {c == a}");

 

仅通过返回类型不足以区分重载的版本。

使用命名参数时,可以改变变量的顺序,编译器会重新安排,获取正确的顺序。顺序不改变时,命名参数可以不拖尾,顺序改变时,命名参数后面不能有非命名参数。

必须为可选参数提供默认值,且可选参数必须拖尾,有多个可选参数时,可以使用命名参数传递任何数量的任意可选参数。

使用 params 声明个数可变参数,其参数类型是一个数组,但可以传入数组和任何数量的变量,params 只能使用一次,且必须是最后一个参数。

 

对于类,如果没有提供任何构造函数,则编程器会在后台生成一个默认的构造函数,它会把所有的字段初始化为默认值,但只要提供了任何构造函数,编译器就不会自动提供默认的构造函数,构造函数不一定是 public 的。

如果没有任何公有的或者受保护的构造函数,类便不能通过 new 运算符实例化,在以下情况有用:

(1)类仅用作某些静态成员或属性的容器,因此永远不会实例化它。这时可以用 static 声明类。

(2)类仅能通过某个静态成员函数来实例化(类工厂方法)。实现一个单例模式的代码如下:

public class Singleton
{
    private static Singleton instance;
    private int _state;
    public int State
    {
        get => _state;
        set => _state = value;
    }
    private Singleton(int state)
    {
        _state = state;
    }
    public static Singleton Instance => instance ?? (instance = new Singleton(8));

}

可以使用构造函数初始化表(类似C++)在构造函数执行前调用另一个构造函数,如果是本类的构造函数,使用 this 。如果是基类的构造函数,使用 base 。初始化表不能有多个调用。例如:public Car(string name) : this(name, 4);

可以给类编写无参数的静态构造函数,这种构造函数只执行一次,比如需要在第一次使用类之前,从外部源中初始化静态字段和属性。.NET 运行库并不确保什么时候执行静态构造函数,但一定确保其至多执行一次,且在代码引用类之前调用它。通常在第一次调用类的任何成员之前执行静态构造函数。

静态构造函数没有访问修饰符,因为其他代码从不显式调用它,只能由 .NET 运行库调用。其也不能带任何参数,一个类也只能有一个静态构造函数,静态构造函数中只能访问类的静态成员。无参的实例构造函数和静态构造函数可以在一个类同时定义,由于执行时间不同所以并不会矛盾。

多个类的静态构造函数的执行顺序是不确定的,静态字段的默认值在调用静态构造函数前分配。

 

结构是值类型,不是引用类型,储存在栈中或者储存为内联(如果它们是储存在堆中的另一个对象的一部分)。结构不支持继承。

new 运算符用于结构时,并不分配堆内存,而只调用相应的构造函数,根据参数初始化所有字段。结构在使用之前,所有元素都必须初始化。

结构的好处在于,为结构分配内存时非常快,因为它们将内联或者储存在栈中,结构超出作用域被删除时速度也很快,也不需要等待垃圾收集。结构的坏处是,当结构作为参数值传递或者结构之间赋值时,结构的所有内容都被复制(对于类来说只复制引用)。结构主要用于小的数据结构。

使用 ref 传递结构时,可以避免性能损失,代价是被调用的方法可以改变结构的值。

大多数结构类型都是不可变的,使用 readonly 修饰结构可以让编译器保证结构体的不变性,从而生成优化的代码,使其在传递结构体时不会复制结构的内容,相反,编译器使用引用(尽管没有使用 ref )。

结构不能继承。但每个结构都派生于 System.ValueType 类,而 System.ValueType 类派生自 System.Object 类。

结构的默认构造函数把所有字段都初始化为默认值,且编译器总是隐式给出无参数的默认构造函数。不能为结构另外提供无参数的构造函数。

 

值类型使用 ref 修饰参数时,传递引用。类类型使用 ref 修饰参数时,传递引用的引用。

如果一个方法需要返回多个值,有如下方法:

(1)使用类和结构(2)使用元组(3)使用 out 关键字。

out 修饰的参数,可以在方法内初始化,但其作用域在方法调用后依然有效。

in 修饰参数,可以确保其在方法中不会被改变(类似C++的 const 修饰参数)。编译器可以优化代码,传递引用(即使没有使用 ref ),从而提高性能。当 in 修饰引用类型时,可以改变变量的内容,但不能改变引用本身。

 

可空类型是可以为空的值类型。普通类型可以隐式转换为可空类型,但可空类型只能显式转换为普通类型。可空类型的 HasValue 和 Value 属性,可以用来判断可空类型是否为空,并获取其值。

 

枚举类型使用 enum 关键字定义。默认情况下 enum 的类型是 int 。但是这个基本类型可以改变为 byte,short,long 或其他无符号类型,使用继承语法即可。命名常量的值默认从0开始,可以在枚举定义中修改。泛型方法 Enum.TryParse<T> 可以解析字符串为枚举类型。反过来可以使用 Enum.GetName,但它不是泛型方法,它以枚举类型(Type,使用 typeof 获取)为参数。 Enum.GetNames 以及 Enum.GetValues 同理,返回枚举的字符串数组或值数组。

enum Color : byte
{
    Red = 1,
    Green = 2,
    Blue = 3
}

static void Main(string[] args)
{
    Color col = Color.Red;
    Console.WriteLine($"{Enum.Parse<Color>("Red")} {Enum.GetName(typeof(Color), 3)}");
    foreach (var s in Enum.GetNames(typeof(Color)))
    {
        Console.WriteLine(s);
    }
    foreach (byte b in Enum.GetValues(typeof(Color)))
    {
        Console.WriteLine(b);
    }
}

 

partial 关键字可以修饰类、结构或接口,表明此处定义是部分的,完整的定义由多个源文件的定义合成后得到。部分类可以包含部分方法,部分方法不需要任何实现代码,例如 public partial void Test();如果生成的代码应该调用可能不存在的代码,则可以使用部分方法,扩展部分的程序员决定是否实现它。如果最终没有实现代码,编译器将删除这个方法调用。部分方法必须是 void 类型,这是为了确保编译器在必要的时候可以删除方法调用。

 

扩展方法,用于给对象添加功能但不使用继承(例如类是密封的)。扩展方法是静态方法,但其代码并不在类的源代码中,一般在另一个类(且必须是静态类)中定义。使用 this 关键字和第一个参数指明扩展的类,例如 public static int GetWordCount(this string s) => s.Split().Length; ,该代码扩展了 String 类。即便扩展方法是静态的,必须使用实例方法语法来调用它,其中第一个参数会自动被编译器替换为调用扩展方法的实例。

为了保证编译器能通过 this 匹配类型,需要打开定义扩展方法的静态类所在的命名空间,如果类型还定义了同名的实例方法,扩展方法将不会被调用,类中已有的任何实例方法都优先。

public static class StringExtension
{
    public static String GetLoopString(this string str, int times)
    {
        StringBuilder sb = new StringBuilder(str.Length * times);
        for (int i = 0; i < times; i++)
        {
            sb.Append(str);
        }
        return sb.ToString();
    }
}
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!".GetLoopString(100));
    }
}