C# Programming

C#是面向对象的编程语言,支持面向组件的编程,通过属性、方法和事件来提供编程模型。

C# 特性:垃圾回收 (Garbage collection);异常处理 (exception handling);类型安全 (type-safe) 的语言设计。

基本语法

应用程序 (application)以名为Main静态方法作为程序执行的起点。Main方法中可书写表达式语句或调用类库或自定义的其他方法。

namespace NameSpace{
   class Program{ 
      static void Main(string[] args){
         /* statements ... */
      } // return void or int
      static async Task Main() { }
      static async Task<int> Main(string[] args) { }  // (C# 7.1)
   }   
}   // Libraries and services do not require a Main method.

Main方法:必须为static方法,不需要是public

args为除了程序名称以外的参数。

一个类中最多包含一个名为Main的方法。在一个程序的多个类或结构中,可能都定义了Main方法。这样的情况下,必须利用某种外部机制(如命令行编译器的选项-main)来选择其中一个Main方法用作入口点。

应用程序入口点方法不能位于泛型类声明中。

从C#9开始,可以省略Main方法,将位于全局命名空间的语句作为主函数内容。

变量

变量表示数据的存储位置。

定义变量

<type> varname = value;

变量名称约定

  • 使用驼峰式,即第一个单词以小写字母开始,后续每个单词的首字母采用大写形式。

  • 变量名称不应包含变量的数据类型(在实际代码中可能不能代表实际类型)。

隐式类型本地变量使用 var 关键字进行创建,该关键字指示 C# 编译器去推断类型。

var age = 10;

变量名应使用骆驼式命名法。

常量

对于数学常量,在System.Math类中有比较精确的定义,可以直接使用,例如pie

字面值

在数字后加上后缀,可以将字面值表示成不同类型的数值。

整形
  • 没有后缀:解析成intuintlongulong
  • 具有u后缀:解析成uintulong
  • 具有l后缀:解析成longulong
  • 具有ullu后缀:解析成ulong
  • 十六进制表示:添加前缀“0x

bool类型的字面值:truefalse

浮点型
  • 具有D/d后缀:解析成double
  • 具有M/m后缀:解析成decimal
  • 具有F/f后缀:解析成float

指数记法

  • 使用后缀e,在后缀后面添加一个整数,并在最后加一个后缀,表示整个数的类型。
  • 后缀是不区分大小写的。
字符串

转义字符:C# 将反斜杠固定用于转义序列,如果出现非法转义序列将产生错误。

使用 \uFFFF 转义序列在文本字符串中添加Unicode编码(UTF-16)字符。

字符串的前面使用 @ 指令避免转义。原始字符串:@“this is \ a string.”

当字符串中含有“\”时,后面的字符以及“\”就会被编译程序解析为转义字符。如果要使用带“\”的字符串而屏蔽掉其转义字符的性质,可以在字符串(双引号)前添加字符“@”。

运算符

复合赋值运算符:如 +=-=*=++--

定位增量和减量运算符:++--

同时System.Threading.Interlocked类提供了线程安全的方法Increment()Decrement()

比较运算符:==><>=<=!=;继承自Object的类可以重写Equals方法,实现值比较(例如String类型的==比较的是字符串的值)。

value equality and reference equality

逻辑运算符:&&||、``;

赋值运算符:对变量进行一次赋值会返回一个值,所以可以一次对多个变量进行赋值。

string req,max;
req=max="It would take a miracle."

位运算符:&,|,^,~

移位运算符:>>, <<

op1st >> op2nd

如果第一个操作数为intuint(32 位),则移位数由第二个操作数的低五位给出(op2nd&0x1f,即最多移32位)。 如果第一个操作数为longulong(64位数),则移位数由第二个操作数的低六位给出(op2nd&0x3f,即最多移64位)。 如果第一个操作数为intlong,则右移位是算术移位(高序空位设置为符号位)。如果第一个操作数为uintulong类型,则右移位是逻辑移位(高位填充0)。

op1st << op2nd

如果第一个操作数是intuint(32 位),第二个参数给出移动位数。 移位操作不会导致溢出:不在移位后第一个操作数类型范围内的任意高序位均不会使用,低序空位用零填充。

优先级

  1. 圆括号 (括号内的内容首先执行)
  2. 指数 (System.Math.Pow())
  3. 乘法和除法(从左至右)
  4. 加法和减法(从左至右)

代码

标识符:

By convention, C# programs use PascalCase for type names, namespaces, and all public members.

表达式:由多个字面值和运算符可以构成常量表达式。

代码块:使用{}定义的一行或多行代码的集合。代码块可以包含其他代码块。

“空格”:指的是由 space bar 生成的单个空格、由 tab 键生成的制表符以及由 enter 键生成的新行。C# 编译器会忽略空格。

注释和文档

注释的作用
  • 记下一段代码的意图,有助于描述用途或思考过程;

    勿添加关于单个代码行如何工作的注释,相关信息可通过文档获取;

    不要完全相信注释。 在进行许多更改之后,它们可能不会反映代码的当前状态。

  • 暂时删除应用程序中的代码,以尝试其他方法;

  • 添加类似于 TODO 的消息。

单行注释以字符 // 开头并延续到源行的结尾。

带分隔符的注释 以字符 /* 开头,以字符 */ 结束。带分隔符的注释可以跨多行。

具有特殊格式的注释可用于指导某个工具根据这些注释 和它们后面的源代码元素生成 XML。这类注释是以三个斜杠 (///) 开始的单行注释,或者是以一个斜杠和两个星号 /**) 开始的分隔注释。

流程控制

选择

if-else
if (condition1){ statements }
else if (condition2) { statements }
else {statements}

if-else可以嵌套使用。

switch-case
switch (condition) {
	case value1:
		statements;
		break;
	case value2:
		statements;
		break;
	default:
		statements;
		break;
	}
}

每一个非空的分支必须使用跳转语句(breakreturn等)。

condition不仅可以是整数,还可以是字符串等类型。而判断条件也不仅包括值类型,还包括模式匹配(例如判断是否为整数/常量)。

循环迭代

do-while
while (condition){
    statements;
}
do {
    statements
} while (consition);
for
for (int i = 0; i < args.Length; i++) {
	statements;
}
foreach

foreach语句用于访问数组的所有元素或实现了接口IEnumeralbeIEnumerable<T>的集合对象的所有元素。

foreach(collection_type element in CollectionObject)
{ statements; }

foreach用于迭代遍历集合查找信息,但是不能对源集合增加或删除元素以避免不可预知的后果。如果需要增加或删除元素,使用for循环。

foreach语句块的任何点,都可以使用break跳出循环,或使用continue进入下一步迭代。foreach循环也可以使用gotoreturnthrow语句退出。

yield语句:使用yield语句的方法、操作符或get访问器相当于是一个建议的迭代器。

class MyIterator {
  public IEnumerable MyEnumerator(int start, int end){
    for (int i = start; i < end; i++){
      yield return i;
    } // 当使用foreach语句访问迭代器时,每次执行到yield语句则直接返回,并记录下当前位置
  }   // 下一次迭代则从上次中断位置继续运行。
}     // 可使用一系列yield语句构造迭代器的元素,或使用循环(本示例)实时构造迭代器元素
      // 或通过循环返回迭代器类型的内部成员变量。

跳转

breakcontinuegotothrowreturn

异常处理

try {
    statements;
} catch (Exception e) {
    statements;
} finally { 
    statements; 
}
using语句

fianlly语句块仅存在单个语句用于释放try语句块中申请的资源,则可用using语句代替。

using(File f = File.Open(name)){
   system.WriteLine(f.Readline())
}

类型系统

变量大致具有7种用途,即静态变量、实例变量、数组元素、值参数、引用参数、输出参数和局部变量。

每个变量都具有一个类型,用于确定哪些值可以存储在该变量中。C#具有统一类型系统 (unified type system),所有C#类型(包括intdouble 等基本类型)都继承于单个根类型objectSystem.Object)。

根据变量在内存中的存储方式以及操作方式,可以分为值类型(value-type)和引用类型(class-type)两大类

  • 值类型存储在堆栈中,一般都是直接访问;将一个变量值赋给另一个变量,会在内存中重新开辟一段空间,函数传递参数也会在内存中另外生成一个副本。
  • 引用类型必须在托管堆中为引用类型变量的值分配内存,而引用类型对象的引用(地址)存储在栈中;引用类型是由垃圾回收机制来管理的;

所有值类型均从类System.ValueType隐式继承,后者又从类object继承。System.ValueType本身不是值类型,而是引用类型。(可以看成根类型object中不包含任何数据,因此该类型既可以视为值类型也可以视为引用类型。System.ValueType则定义了数据及其值类型操作规则,从而覆盖了引用类型的规则。其他继承自object的类型因为不包含这一套操作规则,因此具有引用类型的操作规则。)

五种类型是用户可定义的:类 (class type)、结构类(struct type)、接口(interface type)、枚举(enum type) 和委托(delegate type)。

值类型

image-20191109103126523

布尔类型不能与其它类型进行转换;

简单数值类型定义了该类型的取值范围:MaxValueMinValue,可通过GetTypeCode获得类型名。

浮点数floatdouble类型,当一个数除0时,不会出现错误,而是产生“非零”结果,当打印结果时会得到“NaN”;当数值溢出时,也不会产生异常,而是记为“Inf”。当一个数,非常接近与0时,就会被近似为0,根据数的正负,近似后的数可能为“+0”,也可能为“-0”。

字符类型(char):定义了一系列判断字符类型的方法和转换大小写方法。

值相等关系

==运算符比较基本值类型的值是否相等。

结构体

结构类型与类类型相似,结构类型除了是值类型以外,还不支持用户指定的继承,并且所有结构类型都隐式地从类型object继承。结构类型的成员默认访问权限为private,且不能为成员指定初始值,而必须通过构造方法进行初始化(不支持无参数构造方法)。

枚举

每个枚举类型都有一个相应的整型类型,称为该枚举类型的基础类型 (underlying type)。没有显式声明基础类型的枚举类型所对应的基础类型是 int。枚举类型的存储格式和取值范围由其基础类型确定。

enum Alignment: sbyte{	
    Left = -1,	
    Center = 0,	
    Right = 1
}

获取枚举类型的字符串表示:(1)ToString();(2)Enum.GetName()

引用类型

构建C#应用程序的主要对象类型数据。引用类型在默认值都是null

image-20191109105453972

引用相等关系

引用类型在使用“==”或“~=”操作符时,默认比较的是其引用的地址,Object.Equals()等价于“==”。

使用“=”操作符只使得多个引用类型引用同一个对象,即将一个引用类型变量赋值给另一个同类型变量,赋值的是这个类型对象的地址,所以两个引用都指向同一对象;通过任意一个引用修改该对象,都使得对象发生改变。

必须使用new关键字来创建引用类型变量(实际创建了对象),运算结果也会产生新的对象(运算结果也是通过new生成的);

变量引用:variable-reference 表示一个存储位置,访问它可以获取当前值以及存储新值,在 C 和 C++ 中,variable-reference称为lvalue

字符串
数组

委托

委托类型 (delegate type)表示具有特定参数列表和返回类型的方法,即方法视为对象,==委托即方法的类型==。委托是用来处理其他语言(如 C++)需用函数指针来处理的情况的。与 C++ 函数指针不同,C++ 指针仅指向成员函数,而委托同时封装了对象实例和方法。

函数对象(闭包)。

委托声明定义一个从 System.Delegate类派生的类,与方法声明语法一致(除了使用delegate关键字):

[modifier] delegate <type> delegateName(param_list);

委托类型使用函数或匿名函数进行初始化。

public delegate double Calculator(double x, double y);
public double Add(double x, double y);
public double Sub(double x, double y);
Calculator CalAdd = new Calculator(Add);   // 完整初始化写法
Calculator CalSub = Sub;                   // 简洁写法
委托运算

委托实例封装了列表包括一个或多个函数对象。委托对象可使用 "+" 运算符进行合并。一个合并委托调用它所合并的两个委托。只有相同类型的委托可被合并。"-" 运算符可用于从合并的委托中移除组件委托。在执行委托时,调用列表中的方法被依次执行。

Calulator MultiCal = CalAdd + CalSub;
MultiCal(10, 5);      // invoke two Calculator methods

**对于实例方法,可调用实体由该方法和一个相关联的实例组成,若该方法会修改实例,则委托中的可调用实体也会修改对应实例。对于静态方法,可调用实体仅由一个方法组成。**用一个适当的参数集来调用一个委托实例,就是用此给定的参数集来调用该委托实例的每个可调用实体。

Lambda表达式

Lambda表达式是一个匿名函数(anonymous function,可以用于创建委托;可以当作局部函数作为某些函数的参数。

(input parameters) => expression;
(input parameters) => {statement;}

"=>"操作符左边为输入参数列表,右侧为表达式或语句块。参数个数为1时,括号可以省略。可以没有输入参数。

类型转换

类型判断

is关键字判断对象是否为给定类型。

语法:obj is type

type是一个类,而obj也是该类、或继承该类、或封箱到该类中的实例,结果为true

type是接口,而obj也是该接口类型、或继承该接口的类,结果为true

转换

自动转换:子类可以自动向父类转换

强制转换:父类转换为子类需要强制转换

int first = 7, second = 5;
decimal quotient = (decimal)first / (decimal)second;

as关键字:在类型兼容的引用类型之间进行类型转换。

​ 语法:type obj_type = obj as type

​ 当能够进行转换时,执行转换;当不能转换时,返回null

字符串与数值之间转换:使用Parse()方法、System.Convert()ToString()方法。另外TryParse()方法和Parse()方法的区别在于如果转换不成功,不会引起异常而是返回false

装箱和拆箱

引用类型和值类型的相互转换。

char ch = 'c';
Object obj_ch = ch;   // obj_ch boxing the value type ch
char ch2 = (char) obj_ch; // unboxing from ref-type obj_ch

装箱:隐式将值类型转换为 object 类型。

取消装箱:显式地将一个对象类型转换为值类型,使用强制转换方法。这种转换方式必须类型兼容,否则出现异常。

面向对象编程

类的定义语法:

public class ClassName<TypeName,...>: baseClass, Interfaces
{ 
    private int fieldName;
    protected static int StaticField;
    public int propertyName {get{};set{}};
    public void methodName(args){statements;}
    // constructor
    public ClassName(args){statements}
    // indexer
    public TypeName this[int index]
    {statements; return items[index]}
    // event
    public event DelegateType ThingsChanged;
}

class修饰符:newpublicprotectedinternalprivateabstractsealedstatic

当使用new关键字创建对象时,如果使用该对象的位置(函数参数)类型已知,则可省略对象名直接提供参数。

new(0, "Squeaky Bone", 20.99m)

声明了类型参数(TypeName)的类型成为泛型类型。结构类型、接口类型和委托类型也可以是泛型。

如果类型定义时没有指定父类,则类继承于System.Objectobject)。

字段

如果需要一个具有常量值的符号名称,但该值的类型不允许在 const声明中使用,或者无法在编译时计算出该值,则static readonly 字段就可以发挥作用了。

方法

type functionName(args) { /*body*/}
type functionName(args) => single_expr;   // 方法表达式(C# 6)

Lambda表达式是没有函数名的函数表达式。

通常,为了保持程序的简洁,函数仅返回一个值。但如有必要从函数返回多个值,可以采用的方法:(1)创建一个包含多个值的类,并返回该类的对象;(2)将参数声明为ref/out类型。

方法名后的()为函数调用运算符。

参数

值参数:是实际参数的副本。

引用参数(ref)引用参数与实际参数为同一个变量。

输出参数(out)输出参数可引用函数内部的局部变量,当函数返回时变量不会被释放。ref参数必须在传入前进行初始化,而out参数则不必。

参数数组允许向方法传递可变数量的实参。

静态方法

静态方法的操作不需要引用特定对象。

构造函数和析构函数

==静态构造函数==用于初始化类的静态成员。

子类构造函数调用基类构造函数(类似于C++构造方法),使用“:”操作符,base关键字(由于C#是单继承的,因此不需要使用基类名称)。

public ClassName(param_list): base(base_params){
    // 构造函数
}
public ClassName(arg) => Member = arg;      // (C# 7)

如果需要将参数传给基类构造器之前进行修改,可以在base_params中调用相关的函数并返回修改后的值。

析构函数不能带参数,不能具有可访问性修饰符,也不能被显式调用。垃圾回收期间会自动调用所涉及实例的析构函数。

~Destroyer() { /*release resources*/ }
~Destroyer() => expr;    // (c# 7)

对象初始化

除调用构造函数外,可直接提供成员变量初始化列表进行初始化。

var instance3 = new ExampleClass(){
    Name="Desktop", ID=37414, Location="Redmond", Age=2.3 };

等效于调用默认构造函数后,再对相应成员赋值。

属性

属性 (property) 提供对私有成员进行读写、计算的方法,可被视为公共的数据成员,实际上则是特殊的方法(访问器)。属性与字段的声明方法区别在于属性声明了访问器,因此对类的数据实现间接访问,而字段则是对类的数据的直接访问。

modifiers int PropertyName{
    get{ return this.x; }
    set{ this.x = value; }
}
type PropertyName => expr;   // Read-only properties (C# 6)
type PropertyName {          // (C# 7)
   get => member;
   set => member = value;
}

get访问器相当于有返回值的无形参方法。当在表达式中引用属性时,将调用该属性的get访问器以计算该属性的值。没有get访问器的属性是只写的set访问器相当于具有单个形参(以关键字value作为隐形参)和void返回类型的方法。没有实现set访问器的属性是只读的

属性的访问限定符可以是五种中的任意一种。

属性不是变量,因此不能传递作为refout修饰的参数。

使用属性并不一定比直接公开字段效率低。当属性是非虚的且只包含少量代码时,执行环境可能会用访问器的实际代码替换对访问器进行的调用。此过程称为内联 (inlining),它使属性访问与字段访问一样高效,而且仍保留了属性的更高灵活性。

当属性不需要添加额外的读写逻辑时,声明属性的同时,编译器将自动为该属性创建对应的匿名字段,该字段只能被属性的getset访问器所访问。这时属性等效于一个字段。

public type PropertyName { get; set; }

当属性声明包含static修饰符时,称该属性为静态属性 (static property);反之,该属性为实例属性 (instance property)。静态属性不与特定实例相关联,因此在静态属性的访问器内引用 this会导致编译时错误。静态属性需要通过类名进行访问,而实例属性需要通过实例名称进行访问。

属性的访问器可以是虚的。当属性声明包括 virtualabstract override修饰符时,修饰符应用于该属性的访问器。

索引器(Indexer)

索引器的定义方式类似属性。

private string[] types = { "Baseball", "Basketball", "Football"};
public string this[int i]{
   get => types[i];
   set => types[i] = value;
}

事件

事件 (event) 是一种使类或对象能够提供通知的成员。事件的声明与字段类似,不同的是,事件的声明使用event关键字修飾,值类型必须是委托类型,表示事件的处理方法。

public event DelegateType SampleEvent;

使用?.运算符发起事件。

SampleEvent?.Invoke(sender, event_args);
SampleEvent(sender, event_args)

事件参数:根据发起事件所对应的委托类型的参数数量和类型决定。

可使用任意类型和数量的参数作为事件参数。

EventArgs类是表示事件参数的基类,不包含任何数据。可从该类导出自定义类型将所有参数作为成员变量包括其中。使用单一类型作为事件处理方法的参数,便于代码自动生成。

事件处理函数(event handler)

委托类型可以进行加减操作,因此可以为事件添加一个或多个处理方法。事件处理方法签名与事件的委托类型一致。

void event_handler(object sender, EventArgs e){}
sender.SampleEvent += listener.event_handler;

事件处理方法可以是发布事件的类(sender)中的成员函数,也可以是其他类的成员函数。在其他类中订阅发布者的事件必须能够获取发布者类的引用(sender)。

委托实际还封装了成员函数及其所属类型的实例,因此事件发布和订阅在实例对象之间进行。

解除订阅:使用-=运算符从事件委托中删除事件处理函数。

异步事件处理

事件的发送和接收是同步的,即事件委托中所有事件处理函数被执行。为了实现异步,应该在将事件处理函数实现为异步方法

运算符

可以定义三类运算符:一元运算符、二元运算符和转换运算符。

所有运算符都必须声明为public static

访问限定符

访问限定符用于声明类成员的访问能力。访问限定符包括以下四类:

  • public
  • protected
  • internal
  • private

使用以上访问限定符可以声明以下五类访问级别:

  • public:访问不受限制。
  • protected:访问限制在本类型,或由本类派生的类型。
  • internal:访问限制在当前程序集。
  • protected internal:访问限制在当前程序集,或由本类派生的类型。
  • private:访问限制于包含类型。

多态

类支持单一继承和多态,这些是派生类可用来扩展和专用化基类的机制。

虚方法

使用“virtual”关键字修饰的方法,用于实现多态。当调用虚方法时,运行时将确定调用对象是哪一个子类的实例,并调用适当的覆盖方法。

通过override关键字在子类中声明覆盖父类的虚方法。使用override关键字时不能同时使用newstaticvirtual关键字修饰方法;使用override修饰符时,父类必须具有同名虚函数或抽象方法。(另一种覆盖父类方法的关键字是new,使用new关键字时,父类和基类中需要有同名的函数。这个过程叫做方法的隐藏。重写虚方法可以使用overridenew,重写抽象方法只能用override。)

抽象方法

用于抽象表示公共的方法,将具体的实现交给具体的类。

抽象 (abstract) 方法是没有实现的虚方法。用“abstract”关键字修饰的方法,只有函数原型,不能添加函数体。声明抽象方法的类必须被声明为抽象类(以“abstract”关键字修饰的类)。

在子类中,使用overide关键字重写父类中的抽象方法,同时该函数在父类中不能是私有成员。

不能用 “sealed”修饰符修饰抽象类,因为这两个修饰符的含义是相反的。

方法重载

同一类中的多个方法具有相同名称,条件是这些方法具有唯一的签名(signature)。

接口

接口 (interface) 定义了一个可由类和结构实现的协定,可以包含方法、属性、事件和索引器。

一个接口可以从多个基接口继承,而一个类或结构可以实现多个接口。

当类或结构实现某个特定接口时,该类或结构的实例可以隐式地转换为该接口类型。

反射

Function

obtain information about loaded assemblies and the types defined within them, such as classes, interfaces, and value types.

Classes, Assembly, Module, ConstructorInfo, MethodInfo, FieldInfo, EventInfo, PropertyInfo, ParameterInfo, CustomAttributeData.

create type instances at run time, and to invoke and access them.

nameof(identifier):生成变量、类型、成员的字符串表示名称。

using System.Reflection;
using System.Type;

运行时中的泛型

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/generics-in-the-run-time

泛型和反射

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/generics-and-reflection

特性(Attributes)

特性将元数据、声明性信息与代码(例如程序集、类型、方法、属性等)关联。可通过反射在程序运行时获取这些信息。

在代码声明前添加特性标签:

[Serializable]
public class SampleClass{ 
  // Objects of this type can be serialized.
}
[System.Runtime.InteropServices.DllImport("user32.dll")]
extern static void SampleMethod();

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/

内置属性
using System.Attribute;
[Required] string Name;               // 该属性非空
[Range(0.01, 9999.99)] decimal Price; // 数值范围

程序结构

C#程序由一个或多个源文件(编译单元)组成,在逻辑上按命名空间进行组织。命名空间中定义类,类的定义中包含各类型成员。

命名空间

命名空间(namespace)中定义以下内容:

  • 嵌套命名空间
  • 接口
  • 结构
  • 枚举
  • 委托

定义命名空间

namespace identifier {……}

命名空间隐式地具有公开(public)访问属性且不可被修改,在声明时不允许使用任何访问修饰符。它要么出现在编译单元第一行,要么作为成员出现在其他名字空间的声明中。

命名空间的声明空间是“开放式的”,两个具有相同的完全限定名的命名空间声明共同构成同一个声明空间,因此命名空间可以跨文件进行组织。

无论你是否在源代码中声明一个命名空间,编译器都会添加一个默认的命名空间。该命名空间有时称为全局命名空间,在每个文件中都出现。全局命名空间中的任何标识符在有名的命名空间中都可访问。

引用命名空间中的内容

程序可以直接使用自身所在的命名空间内的名称。如果在程序中使用其他命名空间中的内容,可以将该命名空间引入当前程序。

using Namespace;

可使用该空间中的所有内容。

或者通过命名空间名称索引要使用的内容

space1.space2.class1

命名空间之间的层次结构只是一种组织上的形式:当引用父空间时,子空间对于引用程序来说是透明的,如果还要使用子空间需要另外声明引用。同时注意:如果在父空间和子空间都声明了同名的类,则同时引用两个空间,会导致引用混乱,编译器无法确定程序究竟是要使用哪一级的类。这是只能通过指明类的命名空间来进行访问。

定义命名空间别名:

using co = Company.Proj.Nested;

.NET Framework的命名空间包括:SystemSystem.Windows.FormsSystem.Drawing ……

Partial Class

将类、结构或接口的定义分离为多个部分到多个文件中。每个部分都必须使用partial关键字。每个部分必须在编译时可用,每个部分具有相同的访问权限,例如publicprivate等。

如果某个部分定义为abstract,则整个类被认为是abstract。如果某个部分定义为sealed,则整个类被认为是sealed。如果某个部分声明了基类,则整个类继承该基类(所有部分必须声明一致的基类,但是省略基类声明的部分仍然继承该基类。)。

每个部分可以声明不同的基类接口,且最终类型必须实现所有由每个部分声明的接口。任何类、结构、接口成员在每一个部分中都可用。最终类型是所有部分的组合。

以下几种情况应该将类的定义分离为多个部分:

  • 大型项目,将类分为几个文件允许多个程序员同时工作。

  • 自动生成源代码的情况。

Partial Methods

局部类可以包含局部方法。类的一个部分可以包含方法的原型,可选的方法实现可以在同一个部分或其他部分。如果没有提供方法的实现,则该方法和该方法的所有调用在编译时被移除。

局部方法必须以partial修饰,并且返回void。

局部方法可以具有ref但是不能有out参数。

局部方法是隐式的私有方法,因此不能是抽象方法(virtual)。

局部方法不能被extern修饰,因为方法体的出现与否决定该方法是否被定义或实现。

局部方法可以具有staticunsafe修饰符。

嵌套类型

在类或结构声明内部声明的类型称为嵌套类型 (nested type)。在命名空间内声明的类型称为非嵌套类型 (non-nested type)。

异步编程

https://docs.microsoft.com/en-us/dotnet/csharp/async

Task-based Asynchronous Pattern (TAP)

The async keyword turns a method into an async method, which allows you to use the await keyword in its body. The await keyword ==yields control== to the caller of the method that performed await, and it ultimately allows a UI to be responsive or a service to be elastic.

The compiler transforms your code into a state machine that keeps track of things like yielding execution when an await is reached and resuming execution when a background job has finished.

IO密集型任务:await an operation that returns a Task or Task<T> inside of an async method.

your code be "waiting" for something

public async Task<int> GetDotNetCountAsync(){
    var html = await client.GetStringAsync("https://dotnetfoundation.org"); 
    return Regex.Matches(html, @"\.NET").Count;
} // await GetDotNetCountAsync() at somewhere
public async Task<string> GetHtmlAsync(){
    var client = new HttpClient(); // Execution is synchronous here
    return client.GetStringAsync("https://www.dotnetfoundation.org");
}

image-20201119233012960

在一个方法中如果有后续计算依赖于异步方法的返回结果,应当使用await关键字。此时,await将创建并返回一个新的Task<T>对象,表示一个可能尚未完成的任务。await关键字之后的代码将封装到Task<T>对象中,稍后由系统调度并通过Task的方法和属性进行监视。此任务稍后完成时,将计算结果返回更新之前无计算结果的Task<T>对象。

此处的返回机制类似于fork方法,即进程在此处分支,父进程立即返回Task<T>,子进程的代码则是await语句之后的内容。但是子进程不是立即启动执行,而是由系统管理。当系统收到await的任务执行完成时,将该任务的返回结果解封,并传递给子进程继续执行。

==一个方法如果使用了await关键字,则必须在方法声明上添加async修饰==;

一个方法如果调用了异步方法,但没有await获取结果,则该方法应该直接在异步方法处返回(后续代码依赖于异步方法返回的结果,否则也无需写在异步方法之后);该方法中没有后续需要执行的代码,因此无需使用await创建一个新的Task<T>对象。

异步方法的起点Task<T>的返回方式使得异步方法层层嵌套,并最终由系统调用进入内核启动真正的异步过程。例如系统与网络设备的交互,设备在收到系统指令后,准备通过网络发送数据,并向系统返回该过程的状态为挂起(pending),系统因此可以暂时从该任务返回执行其他任务。当请求完成且收到来自设备驱动的数据后,设备通过中断信号(interrupt)通知系统(CPU),系统的中断处理器将接收数据并向上传递到注册的任务(通过system interop call),这些结果将缓存在队列中直到将其解封并交给空闲线程执行任务中的剩余代码。

异步过程实现的基本原理,仍然是并行处理模型。系统(CPU)和设备是并行运行的,两者间通过总线等方式进行通信。系统(CPU)内部内核进程和用户进程也是并行的,通过系统调用进行数据交换。

TaskCompletionSource.

共享线程池:异步方法在await之后将返回,从而释放占用的线程,可以由其他任务调用。而当异步任务完成后,系统将从线程池中分配线程以运行Task<T>对象中的剩余代码。当一个Task<T>执行完成时,将更新其上层Task<T>的状态,将其加入缓存队列,从而使得上层任务在未来某个时刻被空余线程执行。由此,层层向上经过一系列离散时间片段完成异步任务,直到顶层异步方法。==因此,异步方法真正占用线程的时间被大幅减少,从而提高系统的服务能力。==对于客户端而言,为了增加UI的响应能力,需要将耗时的计算或IO任务分配给额外线程。使用共享线程池可大幅减少手动创建专用线程的开销。对于IO任务而言,由于其并不消耗CPU资源,因此分配其单独的线程将严重浪费CPU资源。

异步方法的终点:在await之后输出结果(终端、屏幕或文件等),而非再返回到上一层调用者。这意味者该方法的返回类型为Task<void>,这通常是事件处理器(event handler)。

async Task<void> should only be used for event handlers.

事件处理器的调用者(事件队列管理器)无需等待事件处理器执行完成并查看其结果。

CPU密集型任务:await an operation that is started on a background thread with the Task.Run method.

your code be performing an expensive computation

spawn off the work on another thread with Task.Run

concurrency and parallelism: Task Parallel Library

calculateButton.Clicked += async (o, e) => {
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
}
Wait for multiple tasks to complete

可以首先构造多个异步任务,再等待其执行完成。由于每个异步任务会在wait语句处立即返回,因此启动多个任务的延迟不会太大。待所有任务启动后,再等待最先结束的任务。

var eggsTask = FryEggsAsync(2);    // => Task<Egg>
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var eggs = await eggsTask;
var bacon = await baconTask;
var toast = await toastTask;

这些任务可同时被分发执行,可以首先等待预计最先完成的任务。

Task.WhenAll and Task.WhenAny

public static async Task<List<User>> GetUsersAsync(List<int> userIds){
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds){
        getUserTasks.Add(GetUserAsync(userId));
    }
    return Task.WhenAll(getUserTasks);  // similar for `Task.WhenAny` 
}

await Task.WhenAny类似于Linux的selectepoll

Tips
  • You should add "Async" as the suffix of every async method name you write.

  • Consider using ValueTask where possible

  1. Deeper Dive into Tasks for an I/O-Bound Operation.

  2. Asynchronous programming with async and await

  3. Task asynchronous programming model

  4. Asynchronous programming patterns

Task and Task<T>

Promise Model of Concurrency.

Task and Task<T> objects supported by the async and await keywords.

  • Task represents a single operation which does not return a value.
  • Task<T> represents a single operation which returns a value of type T.

By default, tasks execute on the current thread and ==delegate work to the Operating System==, as appropriate. Optionally, tasks can be explicitly requested to ==run on a separate thread== via the Task.Run API.

构造Task

预处理指令

自动内存管理

不安全代码

指针

附录

关键字

const:如果变量是const,则该变量也是static的。

using:声明语句块,用于保证具有Idisposable接口的对象(非托管资源)的正确使用。

using (declare DisposableObjects){ 
    statements; 
}

在语句块中,定义的对象为只读,不能修改或重新赋值。