Csharp临时笔记

1 阅读23分钟

比较系统地 学习C#的语法、特性

TARGET DECK: CSharpLearning

开始使用

十进制类型 decimal

问题

为什么decimal类型不存在精度损失? #flashcard
decimal类型将十进制数的整数部分和小数部分分开转换,单独储存,所以不存在精度问题

浮点数产生误差累积的原因 #flashcard
数据的二进制尾数长度超过了浮点的所能表示的尾数位数,根据📌舍入规则,舍入的操作造成 了误差。

float double decimal 三者的字节数📌 #flashcard
float 含4字节; double 8字节;decimal 16字节

概述
  • 类型 decimal , 后缀m
  • decimal类型范围比double小,但不存在精度损失,常用于银行账目
内容

浮点数表示小数时造成误差的过程:
浮点数通过 尾数 \times 2^{指数} 来表示实数。📌
以32位的float 为例; 其中 1位 符号位,8位 指数位, 23位 尾数位;
当把 131072.68d 以float表示时,首先将小数点左右边的数分别转为二进制:
1(17个0).101011100...
此时尾数为 1.(17个0) 1010111... ,第24位为1; 根据舍入规则,0舍1入,存入的23位尾数为:
(17个0) 1011000 ,此时所表示的数值就变成了 131072.675d了

列表 List

构造函数
List()

属性
Capacity 内部数据结构不变动的情况下 保留的最大元素数
Count 元素数

方法
void Add(T) 在末尾添加元素
void Clear() 清空列表
boolean Contain(T) 是否有元素
int32 IndexOf(T) 找对应元素的索引
boolean Remove(T) 删除某个元素
void Reverse() 翻转列表
void Sort() 排序, 自省排序,O(n logn) 操作📌
T[] ToArray() 将元素复制到一个数组

经验总结

看代码的时候, 着重关注 初始化实例 的部分, 更容易抓住整体的脉络

在Main()里写 WriteLine()的话,其他的 方法中就不要做这个事;
意思就是要 各干各的,职能分明。

vscode 快捷键
End 定位到行尾
crtl shift k 删除行
alt \uparrow \downarrow 移动代码行
ctrl k c / k u 注释行
ctrl ; 末尾加; // 自行添加的
ctrl [ / ] 整行进格, 整行退格
ctrl k 0/ k j 折叠/展开 所有行


基础

程序结构

C#的通用结构

  • C#程序由一个或多个文件组成。每个文件包含零个或多个命名空间。
  • 命名空间包含 类、 结构、 接口、 枚举、 委托、 其他的命名空间。
  • 使用顶级语句作为程序入口; 📌
  • 使用静态方法Main() 作为程序入口

Main()

问题

将字符串转换为数字类型,可以使用哪些方法? #flashcard
比如long的转换,用 Convert.ToInt64(s)、long.Parse(s)、long.TryParse(s,out t)

概述
  • C#程序只能有一个入口点。 多个Main() 使用 StartupObject 编译器选项。
内容
static void Main(string args[]){
	...
}
long num = Convert.ToInt64(args[0]);
// 或者
long num = long.Parse(args[0]);
// 尝试转换
long num;
bool test = long.TryParse(args[0], out num);
if (!test) {Console.WriteLine("输入合规的参数"); return 1;}

暂时看不懂的概述: 📌
顶级语句(未看) 📌


类型系统

通用类型系统 CTS

问题

类型中可储存的信息包括?(6) #flashcard

  • 储存空间
  • 最大值,最小值
  • 运算种类
  • 成员 (字段,属性,方法,事件)
  • 接口
  • 继承自的基类型

内置类型有哪些? #flashcard
整数、浮点、字符文本、布尔值等

自定义类型有哪些? #flashcard
struct enum class interface record

CTS是什么? #flashcard
所有类型最终都派生自单个基类型(System.Object),这样的统一类型层次结构,为CTS

CTS的两个基本点? #flashcard

  • 支持继承原则
  • 所有类型被定义为 值类型、引用类型

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

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

值类型有哪些?以及值类型的特点(4) #flashcard
值类型分为 struct 和 enum 、record struct

  • 值类型变量直接包含其值。
  • 结构的内存 在声明变量的任何上下文中进行内联分配。
  • 对于值类型变量,没有单独的堆分配或垃圾回收开销。
  • 值类型已密封,不能再派生出其他类型。

引用类型有哪些? #flashcard
class interface array delegate record

interface该如何实例化? #flashcard
interface不能实例化,而是向其分配实现接口的类实例。

泛型类型是什么? #flashcard
用作具体类型的 占位符,客户端代码在 创建类型实例时 提供具体类型,该类型为 泛型类型

隐式类型是什么? #flashcard
由var关键字指示编译器 通过初始化语句右侧的表达式 来推断变量的 类型。
var 只可用于局部变量。

匿名类型是什么? #flashcard
将一组属性封装到一个对象当中,无需显式定义类型的类型。
其类型名由编译器生成,每个属性类型由编译器推断。

简述可以为null的值类型。 #flashcard

  • 值类型后追加 ?
  • 是 泛型结构类型 System.Nullable的实例
  • 在数据传入、传出数据库时用到

编译时类型、运行时类型分别是什么? #flashcard

编译时类型: 源代码中变量的声明或推断类型
运行时类型: 变量所引用的 实例类型。

string是哪种类型?有哪些特殊之处? #flashcard

string是引用类型。string类型的对象具有不可变性,在对其值做出更改时,编译器会新建一个string对象,变量会指向这个新对象。这也是string效率低下的原因。要经常改变string的值,应当使用StringBuilder。

概述
  • C#是一种强类型语言。
  • C# 中 bool 不能转换为 int
  • 编译器会自动执行不会导致数据丢失的类型转换。 如果类型转换可能会导致数据丢失,必须在源代码中进行 显式转换。
  • 记录类型可以是引用类型 ( record ) 或值类型 ( record struct )。
  • 内置的数值类型是结构,具有可访问的字段与方法。一般视为简单的非聚合类型,为其声明并赋值。
  • 不能将结构定义为从任何用户定义的类或结构继承,因为结构只能从 System.ValueType 继承。
  • 一个结构可以实现一个或多个接口。
  • 所有枚举继承自 System.Object \rightarrow System.ValueType \rightarrow System.Enum
  • 适用于结构的 所有规则, 同样适用于枚举。
  • 在声明变量为引用类型时,它将包含值 null,直到你将其分配给该类型的实例,或者 使用 new 运算符创建一个。
  • 无法使用 new 运算符直接实例化 interface。 而是创建并分配实现接口的类实例。
  • 所有数组都是引用类型,即使元素是值类型,也不例外。
内容

图 : CTS 中值类型和引用类型之间的关系 值类型和引用类型.png

public struct StuInfo{
	public string stuName;
	public int stuID;
	public StuInfo(string name, int id){
		stuName = name;
		stuID = id;
	}
}
byte num = 0xA;
int t = 114514;
public enum Rank{
	recruit = 1,
	veteran = 2,
	elite = 3,
	champion = 4,
	legend = 5,
}
MyClass c = new MyClass();
IMyInterface i = c;
// or ...
IMyInterface i2 = new MyClass();
string s = "result: " + 114514.ToString();
// or ... 
var type = 1919810.GetType();

公共语言运行时(CLR)是一套完整的、高级的虚拟机 ,它被设计为用来支持不同的编程语言,并支持 它们之间的互操作。📌
编译器将类型信息作为元数据嵌入可执行文件中。 公共语言运行时 (CLR) 在运行时使用 元数据,以在分配和回收内存时进一步保证类型安全性。📌

但是,一个结构可以实现一个或多个接口。 可将结构类型强制转换为其实现的任何接口类型。
将 导致“装箱”操作,以将结构包装在托管堆上的引用类型对象内。
当你将值类型传递给使用 System.Object 或任何接口类型作为输入参数的方法时,就会发生装箱操作。
📌


命名空间 namespace

命名空间的用处 #flashcard
组织各种类; 帮助控制类和方法名称的范围

  • global 命名空间是 “根”命名空间
System.Console.WriteLine("hi");
// 改为: 
using System;
Console.WriteLine("hi");
namespace SampleNamespace{
	class SampleClass{
		void SampleMethod(){}
	}
}
// or ... 
namespace SampleNamespace; 
class...
	medthod...

类 class

问题

类的默认访问权限是? #flashcard
interal

类成员有? #flashcard
字段、属性、方法、事件

类的 字段与属性的初始化 的方式有哪些?(4) #flashcard

  • 默认值 , 数字类型为 0 , 引用类型为 null
  • 字段初始化表达式
  • 构造函数参数
  • 对象初始值设定项
  • string is non-nullable reference type, use string? instead of string
概述
  • 类可以从其他任何非密封类继承
  • 类声明包括基类时,会继承基类除构造函数外的所有成员
  • 类可以支持实现一个或 多个接口
  • abstract 抽象类包含抽象方法,抽象方法包括签名定义 但不包含实现。抽象类不能实例化,只能通过 可实现抽象方法 的派生类来使用该类。
内容
public Class Container{
	private int _capacity;
	public Container(int capacity) => _capacity = capacity;
	// 只能是 单个参数
}
public Class Container(int capacity){
	private int _capacity = capacity;
}
public Class Container{
	public required string name { get; set;}
	public required int capacity { get; set;} // 就不用写构造函数了
}
// when initializing: 
Container c = new Container() {name = "xx89", capacity = 128}; // 但是没有自动补全

记录 record

问题

C#中判断对象相等的方法:📌 #flashcard

运算符 ==
obj.Equals(obj)
静态方法ReferenceEquals(obj1, obj2)
静态方法Equals(obj1, obj2)

运算符== 是怎么判断的? #flashcard

对于 值类型, 判断值是否相等; 对于引用类型,判断地址是否相同。
string类是引用类型,但是重写了== 运算和 Equals() 方法,只进行值的比较。

解释obj.Equals(obj) 方法如何判断的? #flashcard

和 == 类似;很多类进行了override, 除string类,匿名类型也重写为了“值”的比较。

解释 静态方法ReferenceEquals(obj1, obj2) 是如何判断的? #flashcard

对于引用类型,判断地址是否相同; 对于值类型, 进行装箱操作,装箱后地址必然不同,返回false。

解释静态方法Equals(obj1, obj2) 是如何判断的? #flashcard

对于Equals(A, B), 首先使用静态方法ReferenceEquals(obj1, obj2)进行判断;
如果结果为 false,再判断A B是否为null;
如果不是都为null, 则调用 A.Equals(B)

record 的两个特性,及其相应的解释。 #flashcard

  • record / record struct

  • 具有 值相等性

    • 记录类型的两个变量类型相同,所有字段与属性也相同,那么这两个变量是相等的。
    • 有别于引用相等性,which means 引用同一个实例的两个变量是相等的。
  • 具有 不可变性

    • 一经实例化,记录类型 对象的所有字段与属性值是不可更改的。

记录与类的区别(5) #flashcard

  • 记录可在主构造函数中使用位置参数来创建和实例化具有不可变属性的类型。 ❓
  • 在类中指示 引用相等性 的方法和运算符(例如 Object.Equals(Object) 和 == )在记录中指示 值相等性。 // 省略不相等性
  • 可使用 with 表达式对不可变对象创建 在所选属性中具有新值 的副本。
  • 记录的 ToString 方法会创建一个格式字符串,它显示对象的类型名称 及其所有公共属性的名称和值。
  • 记录可从另一个记录继承。 但记录不可从类继承,类也不可从记录继承。
概述
内容
public record Person(string name, string[]phoneNumbers);
...
static void Main(){
	string[] phoneNum = new string[2];
	Person p1 = new ("Jane", phoneNum);  // new Person(parameters), Person 可以省略
	Person p2 = new ("Jane", phoneNum);
	Console.WriteLine(p1 == p2); // true , 不同的实例,但记录只会比较值
	Console.WriteLine(Equals(p1,p2)); // true
	Console.WriteLine(ReferenceEquals(p1, p2)); //false
}
public record Person(string name){
	public required string[] phoneNumbers;    // 记录类型 这里的参数需要追加 required 
}
... 
static void Main(){
	Person p1 = new ("Jane") {phoneNumbers = new string[2]};   // 复习下required
	Person p2 = p1 with {}; // 复制p1 到 p2, 二者是不同的实例
	Person p2 = p1 with {name = "Andrew"};  // 修改其中的属性
}

接口 interface

问题

概述一下接口?是什么 #flashcard

  • 接口包含非抽象 class 或 struct 必须实现的一组相关功能的定义。
  • 接口可以为成员定义默认实现。如果定义了 static 方法,此类方法必须具有实现。
  • 接口成员默认是public,可显式指定 可访问修饰符,但是private成员必须要有默认实现。
概述
  • 实现接口的class 或 struct 必须为所有已声明的成员提供实现。
  • 若要实现接口成员,实现类的对应成员必须是公共、非静态,并且具有相同的名称和签名。
  • 在类型中声明的静态成员不会覆盖接口中声明的静态成员。
  • 如果基类实现接口,则从基类派生的任何类都会继承该实现。
内容
Interface IEquatable<T>{
	bool Equals(T obj){
	};
}

public class Car: IEquatable<Car>{
	public string? name {get; set; }
	public int engineQuality {get; set; }

	public bool Equals(Car c){
		return (this.name, this.engineQuality) == (c.name, c.engineQuality);
	}
}

接口可从一个或多个接口继承。 派生接口从其基接口继承成员。 实现派生接口的类必须 实现派生接口中的所有成员,包括派生接口的基接口的所有成员。 该类可能会隐式转换 为派生接口或任何其基接口。 类可能通过它继承的基类或通过其他接口继承的接口来多 次包含某个接口。 但是,类只能提供接口的实现一次,并且仅当类将接口作为类定义的 一部分 ( class ClassName : InterfaceName ) 进行声明时才能提供。 如果由于继承实现接 口的基类而继承了接口,则基类会提供接口的成员的实现。 但是,派生类可以重新实现 任何虚拟接口成员,而不是使用继承的实现。 当接口声明方法的默认实现时,实现该接 口的任何类都会继承该实现(你需要将类实例强制转换为接口类型,才能访问接口成员上 的默认实现)

接口可以包含实例方法、属性、事件、索引器。
接口可以包含静态构造函数、字段、常量或运算符。
非字段接口成员可以是 static abstract
类的属性和索引器可以为接口中定义的属性或索引器定义额外的访问器 📌
接口不能包含实例字段、实例构造函数或终结器。 ❓

泛型 类和方法

泛型类型参数T

  • 使用泛型类型可以最大限度地重用代码、保护类型安全性以及提高性能。
  • 泛型最常见的用途是创建集合类。
  • .NET 类库在 System.Collections.Generic 命名空间中包含几个新的泛型集合类。 应 尽可能使用泛型集合来代替某些类,如 System.Collections 命名空间中的 ArrayList。
  • 可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
  • 可以对泛型类进行约束以访问特定数据类型的方法。
  • 可以使用反射在运行时获取有关泛型数据类型中使用的类型的信息。

示意图:📌

public class GenericList<T> {
	// define Node class  -o- 嵌套类
	private class Node{
		private Node? next;
		private T data;
		public Node? Next{
			get {return next;}
			set {next = value;}
		}
		public T Data{
			get {return data;}
			set {data = value;}	
		}
		
		public Node(T t){
			next = null;
			data = t;
		}
	}
	// constructor
	private Node? head;
	public GenericList(){
		head = null;
	}
	// add to the end of the list
	public void Add(T t){
		Node cur = new Node(t);
		cur.Next = head;
		head = cur;
	}
	// 输出❓
	public IEnumerator<T> GetEnumerator(){
		Node? cur = head;
		while (cur != null){
			yield return cur.Data;
			cur = cur.Next;
		}
	}
}

匿名类型

问题

匿名类型在哪些情况使用?📌 #flashcard

  • 不打算单独声明某一个类型时
  • 只需要某个类型对象的部分属性或方法参与运算时

动态类型 的用途?📌 #flashcard

  • dynamic,编译时类型不确定, 在运行时时通过反射机制 确定相关对象的 类型。
  • dynamic 可以声明 字段、属性、方法参数、方法返回值

如何将两个匿名类型对象写成相同的类型? #flashcard

匿名对象的属性,具有相同的顺序、名称、类型,则编译器将他们视为相同的 类型。

概述
  • 匿名类型是 class 类型,它们直接派生自 object,并且无法强制转换为除 object 外的任何类型。
  • 当同一匿名类型对象的所有属性相同,包括顺序、名称和类型,此时两个实例的值相等。匿名类型的Equals()方法 比较的是值。
  • 匿名类型的辅助功能级别为 internal。不同程序集的匿名类型无法彼此相等。
  • 匿名类型重写ToString方法,显示所有属性名称和值。
内容
// 临时使用 
static void Main(){
	var stu = new {name = "下北泽", id = 114514};
}
// 部分使用
static void Main(){
	Student stu = new Student("下北泽", 1141514, 1919810);
	var s = new {name = stu.name, id = stu.id};
}
public class Student{
	public string name;
	public int id, int age;
	.....// 等等
}
static void Main(){
	var s = GetObject();
}
// 返回一个匿名对象,所以声明方法为动态类型
private static dynamic GetObject(){
	return new {name = "下北泽", id = 114514};
}
var carArrayp = new[] {new {name = "dart", quality = 89}, new {name = "d6", qulity = 77}};
public static void Main(){
	var car1 = new {name = "dart", quality = 89};
	var car1_fixed = car1 with {quality = 100};
	Console.WriteLine(car1);          // { name = dart, quality = 89 }
	Console.WriteLine(car1_fixed);    // { name = dart, quality = 100 }
	
	var car2 = car1 with {};
	Console.WriteLine(Equals(car1,car2));  //true  值相同
	Console.WriteLine(car1 == car2);       //false 地址不同

	Person p1 = new Person("Jane") {phoneNumbers = new string[2]};  //自定义的 记录
	Person p2 = p1 with {};
	Console.WriteLine(Equals(p1,p2));      //true  记录的值相等性
	Console.WriteLine(p1 == p2);           //true  记录的值相等性
}

array 怎么写? ❓


面向对象的编程

类、结构和记录

问题

解释一下封装? 及其作用。 #flashcard

类或结构可以指定自己的每个成员对外部代码的可访问性。 可以隐藏不得在类或程序集外部使用的方法和变量,以限制编码错误 或恶意攻击发生的可能性。

详细列出可以在类、结构或记录中声明的各种成员(10)。 #flashcard

字段、属性、方法、事件、常量、构造函数、终结器、索引器、运算符、嵌套类型。

说明一下static静态类的几个规则(4)? #flashcard

  • 静态类只能包含静态成员
  • 不能使用关键字进行实例化。
  • 在程序加载时,类的一个副本会被加载到内存中,其成员通过类名访问。
  • 类、结构、记录可以包含静态成员。
概述
  • 类、结构、记录可以嵌套在其他类、结构、记录中。
内容

分部类型 ❓
可以在一个代码文件中定义类、结构或方法的一部分,并在其他代码文件中定义另一部分。

扩展方法 ❓
可以通过创建单独的类型来“扩展”类,而无需创建派生类。 该类型包含可以调用的方法,就像它们属于原始类型一样。

对象

问题

对象的本质是什么? #flashcard
对象是 按照 类型分配和配置的 内存块。

概述
  • 类是引用类型,类对象的变量引用对象在托管堆上的 地址。
  • 结构对象的内存在线程堆栈上进行分配,该内存随着声明它的类型或方法一起回收。
内容

公共语言运行时中高度优化了托管堆上内存的分配和释放。
在大多数情况下,在堆上分配类实例与在堆栈上分配结构实例在性能成本上没有显著的差别。

继承

问题

虚方法与抽象方法的区别?(关于继承上的区别) #flashcard

  • 基类将方法声明为 virtual 时,派生类可以使用其自己的实现override该方法。
  • 如果基类将方法声明为 abstract,则必须在直接继承自该类的任何非抽象类中重写该方法。 如果 派生类本身是抽象的,则它会继承抽象成员而不会实现它们。

抽象类概述(可以什么?3) #flashcard

  • 抽象类可以包含一个或多个声明为抽象的方法签名;这些签名指定参数和返回值,但没有任何实现。
  • 抽象类可以不包含抽象成员;但类如果包含抽象成员,则必须声明为抽象。
  • 本身不抽象的派生类必须为基类的任何抽象方法提供实现。

接口概述 #flashcard

  • 接口是定义一组成员的引用类型。 实现该接口的所有类和结构都必须实现这组成员。
  • 接口可以为其中任何成员或全部成员定义默认实现。
  • 类可以实现多个接口,即使它只能派生自单个直接基类。
概述
  • 类可以将自己或成员声明为 sealed, 来防止进一步派生。
  • 派生类可以通过使用相同名称和签名 声明成员来隐藏基类成员。 new 修饰符可以用于显式指示成员不应作为基类成员的重写。

多态性

问题

阐明多态性的两个体现。 #flashcard

  • 在运行时,在方法参数和集合或数组等位置,派生类的对象可以作为基类的对象处理。 在出现此多形性时,该对象的声明类型不再与运行时类型相同。
  • 基类可以定义并实现虚方法,派生类可以重写这些方法,即派生类提供自己的定义 和实现。 在运行时,客户端代码调用该方法,CLR 查找对象的运行时类型,并调用 虚方法的重写方法。
    //虚拟继承的规则

为什么C#中 每个类型都是多态的? #flashcard
每个类型都最终继承自Object

关于虚拟成员,派生类的行为有哪些? #flashcard

  • 重写;派生类重写基类的虚拟成员,提供自己的定义与实现
  • 不重写:派生类继承基类的虚拟成员,保留现有行为;允许进一步的派生类重写
  • 隐藏:派生类可以隐藏 基类实现的成员的 新非虚实现。
概述
  • 字段不能是虚拟的,只有方法、属性、事件、索引器才可以是虚拟的。
  • 无论派生了多少个类,虚拟成员始终都是虚拟的。直到被密封。
  • 如果希望派生类具有与 基类成员中 同名的成员,可以使用new关键字隐藏基类成员。
  • 如果希望访问到被隐藏的基类成员,可以将派生类的实例强制转换为基类的实例。
内容
public class Shape{
	public virtual void Draw(){Console.WriteLine("its shape");}
}
public class Circle : Shape{
	public override void Draw(){
		base.Draw();  // its optional, it just 调用一下基类的方法
		Console.WriteLine("its circle");
	}
}
...
static Main(){
	var shapes = new List<Shape>() { new Shape(), new Circle()};
	foreach (var item in shapes){ item.Draw(); }
}
public class Shape{
	public void Draw() {Console.WriteLine("its shape")};
}
public class Circle : Shape{
	public new void Draw() {Console.WriteLine("its circile")};
}
...
static void Main(){
	Circle c = new Circle();
	c.Draw();   // 调用派生类的方法
	((Shape) c).Draw();  // 强制转换为基类,此时调用基类的方法
}
public class A{ public virtual void AMethod();}
public class B : A{ public override void AMethod();}
public class C : B{ public sealed override void AMethod();}
// 在C类的声明中,sealed密封了方法AMethod(),该方法对从C派生的任何类来说都不是虚拟方法
// 通过new关键字,密封的方法可以由派生类进行替换。 
public class D : C{ public new void AMethod(); }
...
static void Main(){
	A a = new D();
	a.AMethod();  // 此时调用的是 C 中的AMethod() 
}
// 解释: A类型的变量a 调用了 D实例的方法AMethod(), CLR根据D实例的运行时类型,调用AMethod()的重写方法,即C类中被密封了的AMthod()方法
public class A{ public virtual void BaseMethod(); }
public class B : A{
	base.BaseMethod(); // 调用基类的方法, 指直接继承自的基类型 
	public override void BaseMethod(); 
}

建议虚拟成员在它们自己的实现中使用 base 来调用该成员的基类实现。 允许基类 行为发生使得派生类能够集中精力实现特定于派生类的行为。 未调用基类实现时, 由派生类负责使它们的行为与基类的行为兼容。

功能技术

模式匹配📌
弃元
析构元组和其他类型

异常和错误

概述

异常是使用 throw 关键字创建而成。
异常是最终都派生自System.Exception的类型。

public class ExceptionTest{
	static double SafeDivision(double a, double b){
		if (b == 0)
			throw new DivideByZeroException();  // new 关键字创建 异常类型 
		return a/b;
	}
	static void Main(){
		double a = 1, b = 0; 
		double result;
		try{
			result = SafeDivision(a, b);
			Console.WriteLine($"a/b = {result}");
		}catch(DivideByZeroException){
			Console.WriteLine("error: brbrb..");
		}
	}
}

异常概述 📌

使用异常

解释抛出异常的过程
引发异常后,运行时将检查当前语句,以确定它是否在 try 块内。 如果在,则将检查与 try 块关联的所有 catch 块,以确定它们是否可以捕获该异常。 Catch 块通常会指定异 常类型;如果该 catch 块的类型与异常或异常的基类的类型相同,则该 catch 块可处理 该方法。

如果引发异常的语句不在 try 块内或者包含该语句的 try 块没有匹配的 catch 块,则运 行时将检查调用方法中是否有 try 语句和 catch 块。 运行时将继续调用堆栈,搜索兼容的 catch 块。 在找到并执行 catch 块之后,控制权将传递给 catch 块之后的下一个语 句。

一个 try 语句可包含多个 catch 块。 将执行第一个能够处理该异常的 catch 语句;将 忽略任何后续的 catch 语句,即使它们是兼容的也是如此。 按从最具有针对性(或派生 程度最高)到最不具有针对性的顺序对 catch 块排列。

执行 catch 块之前,运行时会检查 finally 块。 Finally 块使程序员可以清除中止的 try 块可能遗留下的任何模糊状态,或者释放任何外部资源(例如图形句柄、数据库连 接或文件流),而无需等待垃圾回收器在运行时完成这些对象。

如果引发异常之后没有在调用堆栈上找到兼容的 catch 块,则会出现以下三种情况之 一:

  • 如果异常存在于终结器内,将中止终结器,并调用基类终结器(如果有)。
  • 如果调用堆栈包含静态构造函数或静态字段初始值设定项,将引发 TypeInitializationException,同时将原始异常分配给新异常的 InnerException 属性。
  • 如果到达线程的开头,则终止线程。

异常处理

一个不具有 catch 或 finally 块的 try 块会导致编译器错误。
catch 块可以指定要捕获的异常的类型,该类型规范称为异常筛选器。

LogException 方法始终返回 false ,使用此异常筛选器的 catch 子句均不匹配。 catch 子句可以是通用的,使用 System.Exception后面的子句可以处理更具体的异常类。 finally 块让你可以清理在 try 块中所执行的操作。 如果存在 finally 块,将在执行 try 块和任何匹配的 catch 块之后,最后执行它。 无论是否会引发异常或找到匹配异常 类型的 catch 块, finally 块都将始终运行。

int GetInt(int[] array, int index){
	try{
		return array[index];
	}
	catch(IndexOutOfRangeException e) when (index < 0){
		throw new ArgumentOutOfRangeException("index cannot be negative.", e);
	} //抛出更详细更具体的异常
	catch(IndexOutOfRangeException e){
		throw new ArgumentOutOfRangeException("index out of range", e);
	}
}
private static bool LogException(Exception e){
	Console.WriteLine(e.GetType());
	Console.WriteLine(e.Message);
	return flase;  // 始终返回false
}
static void Main(){
	try{
		string? s = null;
		Console.WriteLine(s.Length);
	}catch (Exception e) when (LogException(e)) {} // 记录异常 
}
// System.NullReferenceException
// Object reference not set to an instance of an object.
FileStream? file = null;
FileInfo fileInfo = new FileInfo("./file.txt");
try{ 
	file = fileInfo.OpenWrite();
	file.WriteByte(0xF);
}finally {
	file?.Close();
}

创建和引发异常

在哪些情况下应当抛出异常?

  • 方法无法完成其定义的功能时
  • 对某个对象进行不适当的调用时
  • 方法的参数可能引发异常时

从Exception派生创建自己的异常类,此时派生类必须包含的三个构造函数是?

  • 无参构造函数
  • 一个用于设置消息属性
  • 一个用于设置Message和InnerException属性

异常包含一个StackTrace的属性,此字符串包含当前调用堆栈上的方法和名称,以及方法引发异常的位置。// 文件名、行号
异常包含一个Message的属性,此字符串用来解释发生异常的原因。

// 方法无法完成其定义的功能时
static void SampleMethod(SampleClass sc){
	_ = sc ?? throw new ArgumentException("Parameter cannot be null", nameof(sc));
}
// 对某个对象不适当的调用时
public void WriteLog(FileStream logFile){
	if (!logFile.CanWrite){
		throw new InvalidOperationException("logfile cannot be read-only");
	}
	// else do sth...
}
// 方法的参数可能引发异常时
public void SampleMethod(int[] array, int index){
	try{
		int result = array[index];
	}catch(IndexOutOfRaneException e) {
		throw new ArgumentOutOfRangeException("index is out of range");
	}
}

使用async修饰符声明的方法在出现异常时,异常会储存在返回的任务中,直到任务出现时再出现。

返回值为 Task类型的方法 📌

[Serializable]
public class InvalidDepartmentException : Exception
{
	public InvalidDepartmentException() : base() {}
	public InvalidDepartmentException(string message) : base(message) {}
	public InvalidDepartmentException(string messgae, Exception inner) : 
	base (message, inner){}
}

编译器生成的异常

ExceptionDescription
ArithmeticException算数运算异常的基类
OverflowExceptionchecked上下文的算术运算溢出时引发
DivideByZeroException除零
ArrayTypeMismatchException元素类型与数组类型不匹配
IndexOutOfRangeException索引超边界
InvalidCastException基类型显示转换为接口或派生类失败
NullReferenceException尝试引用值为null的对象
OutOfMemoryException分配内存失败;常用于CLR用尽内存
StackOverflowException执行堆栈有过多的方法调用;常用于过深的递归
TypeInitializationException静态构造函数引发异常并且没有匹配的catch子句捕获

编码样式

标识符命名规则与约定

标识符是什么?
分配给类型、成员、变量和命名空间的名称。

标识符的命名规则(3)。

  • 开头只能是 字母或 下划线 _
  • 其他部分可以是 Unicode 字母、数字、连接、组合、格式字符。
  • 用@前缀 来声明与 csharp关键字重名的 标识符。

Pascal大小写 一般用于?

  • class interface struct delegate等类型 ...
  • 字段、属性、事件 的 public 成员
  • record 参数, 因为是公共属性

驼峰式大小写 一般用于?

  • private、internal字段, 并追加_前缀
  • 局部变量
  • 委托的实例
  • private、internal的 static 字段, 并追加 s_前缀; 对于线程静态,追加 t_
  • 方法参数

泛型类型参数的命名约定📌

public record Address(
	string Street,
	string City;)

编码约定

相关工具📌

字符串数据的代码约定

  • 使用字符串内插来连接短字符串
  • 使用StringBuilder来构建长字符串

数组的代码约定

  • 在声明行上 初始化数组,使用简洁的语法
  • 显示实例化时,再使用var

委托的代码约定 📌

string sample = $"{name}, {id}"; //使用字符串内插来连接短字符串
var samplePhase = new StringBuilder();
for (int i = 0; i < 100; i ++){
	samplePhase.Append(sample);  //使用StringBuilder来构建长字符串
}
string[] sample = {"a","b","c",};      // 在声明行上 初始化数组,使用简洁的语法
var sample2 = string[] {"a","b","c"};  // 显示实例化时,再使用var

教程

C# 概念

可为空 引用类型

可为空的引用迁移

方法

迭代器

委托和事件