using
using在C#中有两种主要用法:作为指令(namespace导入)和作为语句(资源管理)
using指令(Namespace导入)
用于导入命名空间,简化类型访问
using System; // 导入 System命名空间
using System.IO; // 导入 System.IO命名空间
using Project.Models; // 导入自定义命名空间
// 使用示例
Console.WriteLine("Hello"); // 不需要写 System.Console.WriteLine
特殊用法
- 静态using(C# 6+)
using static System.Math; // 导入静态类
var x = Sqr(16); // 可直接使用Sqrt而不是Math.Sqrt
- 别名using
using WinForms = System.Windows.Forms;
var form = new WinForms.Form(); // 使用别名
using语句(资源管理)
用于自动管理实现了IDisposable接口的对象资源,确保对象在使用后被正确释放
using (var resource = new DisposableObject())
{
// 使用 resource
} // 自动调用 resource.Dispose()
示例
- 文件操作
using (var file = new StreamWriter("test.txt"))
{
file.WriteLine("Hello");
} // 自动关闭文件
- 数据库连接
using (var conn = new SqlConnection(connectingString))
{
conn.Open();
// 执行数据库操作
} // 自动关闭连接
- 多个资源
using (var res1 = new Resource1())
using (var res2 = new Resource2())
{
// 使用 res1和res2
} // 先释放 res2 再释放 res1
C# 8.0简化写法
using var file = new StreamWriter("test.txt");
file.WriteLine("Hello");
// 当离开当前作用域时自动释放
底层原理
using语句会被编译器转换为类似以下结构
{
var resource = new DisposableObject();
try
{
// 代码块
}
finally
{
if (resource != null)
resource.Dispose();
}
}
使用注意事项
- 必须实现IDisposable
- 异常处理
using (var resource = new DisposableObject())
{
// 即使这里抛出异常,Dispose()也会被调用
}
- 不要重复释放
var resource = new DisposableObject();
using (resouce)
{
// ...
}
// 这里不要再调用 resource.Dispose()
- 异步场景 C# 8.0+ 支持异步using
await using (var resource = new AsyncDisposableObject())
{
// 异步操作
}
适用场景
- 文件 I/O 类(
StreamReader,StreamWriter,FileStream) - 数据库相关(
SqlConnection,SqlCommand) - 网络相关(
HttpClient某些用法) - 图形相关(
Bitmap,Graphics) - 其他实现了
IDisposable的资源
default(T)
default(T)是一个运算符,它返回类型T的默认值
它的作用就是:无论T是什么类型,都给我一个该类型最“默认”、最“基础”的值
默认值规则
default(T)的行为取决于T是值类型还是引用类型,遵循C#语言的默认值规则:
- 对于所有引用类型(
class、interface、delegate、array、string等)
default(T)返回null- 这表示一个“空”引用,不指向任何对象
- 对于值类型(
struct和所有数字类型、bool、char、enum等)
default(T)返回一个所有字段都被设置为各自默认值得实例(即“归零”的实例)- 这通常意味着
- 数值类型(
int,double,decimal等)返回0 bool返回falsechar返回'\0'(空字符)enmu返回(E)0,即使这个枚举值可能没有定义- 自定义的
struct返回一个实例,其中每个字段都被设置为它的默认值
- 数值类型(
default(T)的存在意义
主要原因是泛型。在编写泛型类后方法时,编译器在编译时无法知道类型参数T具体是值类型还是引用类型
假设没有default(T),可能会这样写
public T GetDefaultValue<T>()
{
// 如果T是引用类型,返回null是OK的
// 如果T是值类型,null不是有效值,编译错误
return null; // CS0403:无法将null转换为类型参数T
}
或者
public T GetDefaultValue<T>()
{
// 如果T是引用类型,返回0没有意义
//编译错误
return 0; // CS0029:无法将类型 int 隐式转换为T
}
default(T)优雅地解决了这个问题。它让编译器根据具体的T在运行时决定返回null还是“归零”的值类型实例
实际应用场景和示例
- 泛型类和方法的初始化
这是
default(T)最经典的使用场景
public class DataStroe<T>
{
private T _data;
public DataStore()
{
// 在构造函数中,将_data初始化为T的默认值
// 如果T是 int, _data = 0
// 如果T是 string _data = null
_data = default(T);
}
public bool IsDataPresent()
{
// 比较时也需要 default(T)
// 对于引用类型,这是与null比较
// 对于值类型,这是与0/false等比较
return !EqualityComparer<T>.Default.Equals(_data, default(T));
}
// 使用
var intStore = new DataStore<int>(); // _data初始化为0
var stringStore = new DataStore<string>(); // _data 初始化为 null
}
- 方法的默认返回值 当一个泛型方法需要返回一个“无结果”或“初始”值时
public T FindItem<T>(List<T> list, Predicate<T> predicate)
{
foreach (var item in list) if (predicate(item)) return item;
// 如果没找到,返回T的默认值
// 对于引用类型返回null,对于值类型返回0
return default(T);
}
// 使用
List<string> names = new List<string> { "Alice", "Bob" };
var result = FindItem(names, n => n == "Charlie"); // result 为 null
List<string> numbers = new List<int> { 1, 2, 3 };
var result2 = FindItem(numbers, n => n > 5); // result2 为 0
- 重置或清除值
public void ClearValue<T>(ref T value) => value = default(T); // 将value重置为其类型的默认状态
defalut字面量(C#7.1引入)
从C# 7.1开始,编译器变得足够智能,可以根据上下文推断出T的类型。因此,可以省略<T>,只写default,使代码更简洁
// 之前的写法
int num = default(int);
string str = default(string);
MyGenericMethod<string>(default(string));
// C# 7.1+ 的推荐写法
int num = default; // 编译器知道你要的是 int,所以是 0
string str = default; // 编译器知道你要的是 string,所以是 null
MyGenericMethod<string>(default); // 编译器根据方法参数类型推断出是 default(string)
// 在泛型方法中同样适用
public T GetDefault() => default; // 等价于 default(T)
Expression-bodied
表达式主体是一个从C# 6开始引入并逐步增强的语法糖,旨在让代码更简洁、更易读
核心概念
表达式主题允许使用箭头符号=>来替代传统的大括号{}代码块,将一个成员的定义简化为单个表达式
它的核心思想是:如果一个成员(如方法、属性)的逻辑可以在一行表达式内完成,那么就可以使用这种简洁的语法
历史演变
| Edition | Support |
|---|---|
| 6.0 | 方法、只读属性、运算符 |
| 7.0 | 构造函数、终结器、Getter、Setter、索引器的访问器(get/set) |
| 9.0 | 顶级语句(本质上是Main方法的表达式主体) |
示例
- 方法(Methods)- C# 6+
// 传统写法
public string GetFullName(string firstName, string lastName)
{
return $"{firstName} {lastName}";
}
// 表达式主题写法
public string GetFullName(string firstName, string lastName) => $"{firstName} {lastName}";
=>替换了{ return ...; }- 表达式的结果自动作为方法的返回值
- 只读属性(Read-only Properties)- C# 6+
//传统写法
public DateTime CreatedTime
{
get { return DateTime.Now; }
}
// 表达式主体写法
public DateTime CreatedTime => DateTime.Now;
- 属性访问器(Property Accessors)- C# 7+
C# 7.0允许对属性的
get和set访问器单独使用表达式主体
private string _firstName;
// 传统写法
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
// 表达式主体写法
public string FirstName
{
get => _firstName;
set => _firstName = value;
}
- 构造函数和终结器(Constructors and Finalizers)- C#7+
public class Person
{
private string _name;
// 传统构造函数
public Person(string name)
{
_name = name;
}
// 表达式主体写法
public Person(string name) => _name = name;
// 传统终结器(析构函数)
~Person()
{
Console.WriteLine("Finalized");
}
// 表达式主体终结器
~Person() => Console.WriteLine("Finalized");
}
注意:构造函数通常有多个语句(参数验证、初始化等),所以旨在其逻辑非常简单(如直接赋值)时才适合表达式主体
- 索引器(Indexers) - C# 7+
private string[] _items = new string [10];
// 传统写法
public string this[int index]
{
get { return _items[index]; }
set { _items[index] = value; }
}
// 表达式主体写法
public string this[int index]
{
get => _items[index];
set => _items[index] = value;
}
使用表达式主体的优点
- 简洁性(Brevity):这是最主要的目的。它显著减少了样板代码(如大括号、
return关键字、get/set块),让代码行数更少,更紧凑 - 可读性(Readability):对于简单的成员,表达式主体就像数学公式一样一目了然,意图非常清晰。一眼就能看出“这个属性返回什么”或“这个方法做什么”
- 函数式风格(Functional Style):它鼓励将逻辑编写为简单的表达式链,而不是复杂的语句块,使代码更接近函数式编程风格
- 与Lambda表达式一致:语法上与Lambda表达式
(x) => x * x保持一致,降低了学习成本,让语言更统一
注意事项与缺陷
- 仅限单个表达式:这是最大的限制。表达式主体只能包含一个表达式,不能包含语句(如
if、switch、for、try-catch等)
- 可行:
=> x + y(表达式) - 不可行:
=> { if (x > y) return x; else return y; }(语句块)
调试略有不同:在调试时,整个表达式主体被视为一行代码。无法像在代码块中那样在
return前设置断点。但这通常不是大问题不要过度使用:简洁不应以牺牲可读性为代价
- 适合:简单的计算、直接返回字段或属性、简单的委托
- 不适合:逻辑稍微复杂的方法。强行将多行逻辑塞进一个表达式(例如使用嵌套的三元运算符
?:)会使代码难以阅读和维护。这时应果断换回传统的代码块写法
var
var是C# 3.0引入的一个非常重要的关键字,它允许开发者在声明变量时让编译器自动推断变量的类型
基本语法和使用
// 显式类型声明
string explicitName = "John";
int explicitAge = 25;
// 使用var隐式类型声明
var implicitName = "John"; // 编译器推断为string
var implicitAge = 25; // 编译器推断为 int
var implicitList = new List<string>; // 编译器推断为 List<string>
工作原理
var的工作原理实际上是基于C#的类型推断机制
编译器通过分析var右侧的表达式来推导处变量的类型
这种推断发生在编译阶段,因此var变量的类型在运行时与显式声明的类型是完全一致的
尽管C#是一种强类型的语言,var只是简化了变量声明的过程,但最终类型依然是静态的,且在编译时已知
目的是提高开发效率、减少代码冗余,而不是改变C#的类型系统var在性能上是零成本的,因为在编译期推断,生成的IL代码与显式声明完全相同
适用场景
- 复杂类型声明
// 没有 var 的情况
Dictionary<string, List<Dictionary<int, string>>> complexDict = new Dictionary<string, List<Dictionary<int, string>>>();
// 使用 var 的情况
var complexDict = new Dictionary<string, List<Dictionary<int, string>>>();
- LINQ查询结果
var results = from person in people
where person.Age > 18
select new { person.Name, person.Age };
// 等价于
IEnumerable<<anonymous type>> results = ...;
- 匿名类型
var person = new { Name = "John", Age = 30 };
Console.WriteLine($"{person.Name} is {person.Age} years old");
// 没有 var, 匿名类型无法显式声明
不适用场景
- 不能用于字段声明
public class MyClass
{
// private var myField; // 编译错误!
private int myField; // 正确
}
- 必须初始化
var value; // 编译错误!必须初始化
value = 10;
var value = 10; // 正确
- 不能为null的初始化
var value = null; // 编译错误!无法推断类型
// 解决方案
string value = null;
var value = (string)null;
var value = default(string);
类型推断规则
- 字面量推断
var i = 10; // int
var d = 10.5; // double
var f = 10.5f; // float
var m = 10.5m; // decimal
var s = "hello"; // string
var b = true; // bool
- 表达式推断
var result = GetResult(); // 类型取决于方法的返回类型
var sum = 5 + 3.2;
var concat = "Age: " + 25; // string
示例
- 集合操作
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// 使用 var 让代码更简洁
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
var squareNumbers = numbers.Select(n => n * n).ToList();
// 对比显式声明
List<int> evenNumbersExplicit = numbers.Where(n => n % 2 == 0).ToList();
- 异步编程
public async Task ProcessDataAsync()
{
// 使用 var 简化异步调用
var result = await GetDataAsync();
var processed = await ProcessResultAsync(result);
// 对比显式声明
DataResult resultExplicit = await GetDataAsync();
ProcessedData processedExplicit = await ProcessResultAsync(resultExplicit);
}
- 模式匹配(C# 7.0+)
public void ProcessObject(object obj)
{
// 使用 var 模式
if (obj is var str && str is string)
{
Console.WriteLine($"String length: {str.Length}");
}
// 使用 var 在 switch 表达式中国
var result = obj switch
{
string s => $"String: {s}",
int i => $"Int: {i}",
var unknown => $"Unknown: {unknown?.GetType().Name}"
};
}
最佳实践
- 在类型明显时使用
var - 在复杂类型声明时使用
var提高可读性 - 在LINQ查询和匿名类型中必须使用
var - 在类型不明显时考虑使用显式以提高代码可读性
- 团队应该制定一致的
var使用规范
dynamic
dynamic是C#4.0中引入的关键字,它的核心目的是绕过编译时的类型检查,将类型解析的工作推迟到运行时
静态类型 vs 动态类型
要理解dynamic,首先要明白C#本质上是一种静态类型语言
- 静态类型(如
int,string,MyClass):在编译时,编译器就知道变量的类型。任何不符合类型的操作(比如对一个string变量进行数学运算)都会导致编译错误。这提供了安全性、智能感知(IntelliSense)和性能优化 - 动态类型(
dynamic):编译器对dynamic变量“放手不管”。它假设你在运行时对该变量做的任何操作都是有效的。编译时不会进行类型检查,也没有智能感知。所有的类型解析、方法调用、属性访问等都在程序运行时动态进行
基本用法和语法
dynamic myVariable = 10; // 开始时是整数
Console.WriteLine(myVariable); // 输出:10
myVariable = "Hello, World!"; // 现在变成了字符串
Console.WriteLine(myVariable); // 输出:Hello, World!
myVariable = new List<int>(); // 现在又是一个列表
一个dynamic变量在其生命周期内可以指向不同类型的对象
使用场景
- 与COM互操作(如Office自动化)
这是
dynamic被引入的首要原因。早期操作Excel或Word时,代码非常冗长,需要大量强制转换
没有dynamic时
var excelApp = new Microsoft.Office.Interop.Excel.Application();
excelApp.Visible = true;
// 需要强制转换,而且参数是 ref object,很麻烦
Microsoft.Office.Interop.Excel.Workbook workbook = (Microsoft.Office.Interop.Excel.Workbook)excelApp.Workbooks.Add();
Microsoft.Office.Interop.Excel.Worksheet worksheet = (Microsoft.Office.Interop.Excel.Worksheet)workbook.ActiveSheet;
Microsoft.Office.Interop.Excel.Range range = (Microsoft.Office.Interop.Excel.Range)worksheet.Cells[1, "A"];
range.Value2 = "Hello";
使用dynamic后
dynamic excelApp = new Microsoft.Office.Interop.Excel.Application();
excelApp.Visible = true;
dynamic workbook = excelApp.Workbooks.Add();
dynamic worksheet = workbook.ActiveSheet;
worksheet.Cells[1, "A"].Value = "Hello"; // 代码简洁明了,像脚本语言一样
- 与动态语言(IronPython, IronRuby)交互
当需要在C#中调用IronPython或IronRuby等动态语言编写的代码时,
dynamic是完美的桥梁
// 示例:在 C# 中执行 Python 代码
var engine = Python.CreateEngine();
dynamic scope = engine.CreateScope();
engine.ExecuteFile("my_script.py", scope);
// 调用 Python 脚本中定义的函数
dynamic result = scope.MyPythonFunction(42);
Console.WriteLine(result);
- 处理动态JSON或XML(反序列化位置结构的数据)
当不确定反序列化后的JSON结构时,可以使用
dynamic来轻松访问数据
使用Newtonsoft.Json(Json.NET)
string json = @"{
'Name': 'Alice',
'Age': 30,
'Pets': ['Dog', 'Cat']
}";
// 反序列化为 dynamic
dynamic data = JsonConvert.DeserializeObject(json);
Console.WriteLine(data.Name); // 输出: Alice
Console.WriteLine(data.Age); // 输出: 30
Console.WriteLine(data.Pets[0]); // 输出: Dog
// 甚至可以处理运行时才存在的属性
if (data.Hobbies != null) { // 如果JSON中没有Hobbies属性,这里不会编译错误
Console.WriteLine(data.Hobbies);
}
- 模拟鸭子模型 “鸭子模型”是动态语言中的一个概念:“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子。”即只关心对象有没有某个方法或属性,而不关心它的具体类型
public void MakeSound(dynamic animal)
{
// 只要在运行时,animal有Quack方法,这行代码就能成功
animal.Quack();
}
// 这两个类没有继承自同一个接口或基类
public class Duck
{
public void Quack() => Console.WriteLine("Quack!");
}
public class Person
{
public void Quack() => Console.WriteLine("人叫");
}
// 使用
MakeSound(new Duck()); // 成功
MakeSound(new Person()); // 成功
// MakeSound(new Dog()); // 如果Dog没有Quack方法,运行时这里会抛出异常
优缺点
优点
- 灵活性:可以编写非常灵活、适应多种类型的代码
- 简化代码:在与COM或动态语言交互时,代码变得极其简洁
- 开发效率:在快速原型开发或处理不确定数据结构时,可以提高效率
缺点
- 性能开销:运行时解析类型和方法调用比直接的静态调用慢,因为涉及反射等机制
- 失去编译时安全:这是最大的风险。如果代码中有拼写错误或类型不匹配,编译器不会报错,直到运行时才会抛出
RuntimeBinderException - 失去智能感知:在Visual Studio中,对
dynamic变量不会有自动完成、方法列表等提示,这会影响开发体验和代码可读性
dynamic与var的区别
| 特性 | var | dynamic |
|---|---|---|
| 类型决定时间 | 编译时 | 运行时 |
| 类型安全 | 是,编译器推断出类型后,就等同于该类型 | 否,编译器不做检查 |
| 智能感知 | 有 | 无 |
| 是否可以重新赋值为不同类型 | 否 | 是 |
!(null-forgiving. 空值忽略符)
告诉编译器“确信这里不为空,不要再报可能为null的警告”
示例
Type? t = typeof(string);
object? obj = Activator.CreateInstance(t); // 可能返回 null
C#的可空类型检查系统(nullable reference types)会在这里发出警告:“Activator.CreateInstance可能返回null”
如果这样写
object instance = Activator.CreateInstance(t)!;
编译器会认为:“开发者负责保证这不是null,不再发出警告”
实际行为
!只影响编译器的可空检查,对运行时完全没有任何效果
如果结果真的为null,程序依然会NullReferenceException崩溃
这可能会掩盖潜在的bug,建议只在非常确信值不可能为null时使用
_(Discard,弃元运算符)
在switch语句中作为弃元
在C#7.0引入的模式匹配中,_在switch语句里充当“默认”或“匹配所有“的案例
object obj = 42;
switch (obj)
{
case string s:
Console.WriteLine($"这是一个字符串:{s}");
break;
case int i when i > 0:
Console.WriteLine($"这是一个正整数:{i}");
break;
case int i:
Console.WriteLine($"这是一个整数:{i}");
break;
case null:
Console.WriteLine($"这是 null");
break;
case var _: // 使用_来匹配任何其他情况,但不关心匹配到的值
Console.WriteLine("未知的类型");
break;
// 传统的 default: 也可以,但 case var _: 更侧重于“不关心值”
}
在这里,case var _:捕获了所有未被前面案例处理的情况,并且明确表示不关心这个匹配到的对象是什么。它比default在语义上更强调“忽略”
在元组和结构中作为弃元
当使用元组或进行结构操作时,经常只对其中一部分值感兴趣。_可以用来忽略那些不关心的部分
// 返回一个元组的方法
(string Name, int Age, string City) GetPersonInfo()
{
return ("Alice", 30, "New York");
}
// 只关心姓名和你那零,不关心城市
var (name, age, _) = GetPersonInfo();
Console.WriteLine($"{name} is {age} years old.");
// 甚至可以忽略多个值
var (firstName, _, _) = GetPersonInfo();
这使代码非常简洁,避免了为不需要的变量起名字
在Out参数中作为弃元
在调用带有out参数的方法时,如果不管关心那个输出值,可以使用_来忽略它。这从C#7.0开始被支持
// 例如 int.TryParse 方法
string input = "123"
// 旧方式:即使不关心结果,也必须声明一个变量
int unused;
if (int.TryParse(input, out unused))
Console.WriteLine("解析成功!");
// 新方式:使用_弃元,代码更清晰
if (int.TryParse(input, out_))
Console.WriteLine("解析成功!");
在Lambda表达式中作为参数
在Lambda表达式中,如果某个参数不被使用,可以将其命名为_,以明确表示该参数被故意忽略
// 例如,一个按钮的点击事件,不需要使用 EventArgs 参数
button.Click += (_, _) => Console.WriteLine("Button clicked!");
// 或者对于 Func/Action,如果有多个参数但只能使用一个
Action<int, int> action = (_, value) => Console.WriteLine(value);
action(1, 2); // 只输出 2
注意:在同时忽略多个参数时(如(_, _)),这是允许的。但如果只忽略一个参数,而使用另一个,编译器是能区分开的
作为私有字段的命名前缀(约定俗成)
这虽然不是语言特性,但是在C#社区中也给非常广泛和重要的编码约定
在定义类的私有字段时,很多人喜欢在其名字前加上_作为前缀,以便于将其与同名的局部变量(尤其是构造函数或属性设置器中的参数)区分开来
public class Person
{
private string _name; // 私有字段
private int _age;
public Person(string name, int age)
{
_name = name; // 清晰地区分了参数 name 和字段 _name
_age = age;
}
public string Name
{
get { return _name; }
set { _name = value; }
}
}
这个约定极大地提高了代码的可读性,让人一眼就能看出一个标识符时类的私有字段还是一个局部变量
在数字字面量中作为数字分隔符
从C#7.0开始,_可以在数字字面量中作为分隔符使用,以提高大数字的可读性。它不会影响数字的值
long bigNumber = 1_000_000_000; //1B
double pi = 3.141_592_653_589_793;
uint hex = 0xDE_AD_BE_EF;
byte binary = 0b1101_0101;
这使得数字更容易被人阅读和理解
where & when
when是一个修饰条件的“过滤器”。它的核心作用是“进一步限定匹配的条件”where用于泛型约束,意思是“这个泛型类型或方法,只接受满足某种条件的类型”
switch表达式或语句中的when
C#7.0引入模式匹配(pattern matching)后,when可以用来对匹配成功的模式再加一道过滤逻辑
switch (obj)
{
case int n when n > 0:
Console.WriteLine("正整数");
break;
case int n when n < 0:
Console.WriteLine("负整数");
break;
case int n:
Console.WriteLine("0");
break;
case string s when s.Length > 5:
Console.WriteLine("长字符串");
break;
default:
Console.WriteLine("其他类型");
break;
}
这里when起的作用就是:只有模式匹配成功,并且when后面的表达式为true时,这个case才会命中
它让switch语句从原来的“静态匹配”变成了“模式 + 条件”的组合,就像给case增加了一个“守卫”(guard)
catch子句中的when
它允许在捕获异常时有条件地决定是否处理该异常
try
{
DangerousOperation();
}
catch (IOException ex) when (ex.Message.Contains("disk"))
{
Console.WriteLine("磁盘相关的IO错误");
}
catch (IOException ex)
{
Console.WriteLine("其他IO错误");
}
这样做的好处是:不需要在catch块内部再去if (...) throw;
直接在when过滤阶段就能决定是否进入该块
在异常处理中使用when是非常优雅的方式,因为它不会吞掉异常链的信息(不像try-catch-if-throw那样会重新抛出而改变栈信息)
where泛型约束
1. 限制类型必须实现某接口
void Print<T>(T obj) where T : IDisposable => obj.Dispose();
T必须实现IDisposable,否则编译不通过
2. 限制类型必须继承某类
class Base {}
class Derived : Base {}
void Show<T>(T value) where T : Base => Console.WriteLine("T继承自Base");
3. 限制类型必须有无参构造函数
void Create<T>() where T : new() => var obj = new T();
4. 多重约束(可以叠加)
void DoSomething<T>() where T : BaseClass, IInterface, new()
{
// ...
}
5. 约束多个泛型参数
class Manager<T, U>
where T : class
where U : struct
{ }
record
在C#9之前,只能用class或struct
| 类型 | 语义 | 特点 |
|---|---|---|
| class | 引用语义 | 比较地址(引用),可变 |
| struct | 值语义 | 拷贝传递,性能好但语法笨重 |
如果只想表达“一组数据”,比如Person(Name, Age),但不想写一堆样板代码(Equals、ToString、GetHashCode),而且希望它是不可变的(immutable),同时希望比较的是内容相等,不是内存引用。所以微软在C#9.0引入了record
基本语法
public record Person(string Name, int Age);
这行代码等价于
public class Person
{
public string Name { get; init; }
public int Age { get; init; }
public Person(string Name, int Age)
{
this.Name = Name;
this.Age = Age;
}
public override string ToString() => $"Person {{ Name = {Name}, Age = {Age} }
public override bool Euqals(object? obj) => obj is Person other && Name == other.Name && Age == other.Age;
public override int GetHashCode() => HashCode.Combine(Name, Age);
}
record会自动
- 定义构造函数
- 定义只读属性(
init) - 实现
Equals和GetHashCode(按值相等) - 生成友好的
ToString()
值相等(Value Equality)
var p1 = new Person("Alice", 18);
var p2 = new Person("Alice", 18);
Console.WriteLine(p1 == p2); // True
区别于class
- 普通
class比较的是“引用是否相同” record比较的是“字段内容是否相同”
这也是它被称为“record type(记录类型)”的原因
不可变对象(Immutable)
record默认使用init访问器,而不是set
var p = new Person("Alice", 18);
// p.Age = 10; // 不允许修改
但可以通过with表达式复制并修改
var p2 = p with { Age = 19 };
这会创建一个新对象,原对象不变
with表达式(复制表达式)
这是record的灵魂搭档
var p1 = new Person("Alice", 18);
var p2 = p1 with { Name = "Bob" };
Console.WriteLine(p2); // Person { Name = Bob, Age = 18 }
它相当于
var p2 = new Person(p1.Name, p1.Age);
p2.Name = "Bob";
继承与Record Class / Record Struct
默认
public record Person(string Name);
等价于public record class Person(string Name)
如果想要值类型(避免堆分配)
public record struct Point(int X, int Y)
record class:引用类型,按值比较record struct:值类型,按字段比较
解构(Deconstruction)
var person = new Person("Alice", 18);
var (name, age) = person;
Console.WriteLine($"{name}, {age}");
C#会自动生成Deconstruct方法
public void Deconstruct(out string Name, out int Age)
{
Name = this.Name;
Age = this.Age;
}
继承与Record层次结构
Record支持继承,并保持值相等的正确行为
public record Person(string Name);
public record Student(string Name, int Grade) : Person(Name);
var s1 = new Student("Alice", 1);
var s2 = new Student("Alice", 1);
Console.WriteLine(s1 == s2); // True
但注意:
record的相等比较是基于类型 + 内容的 即使内容相同,不同类型也不相等
可变record(不推荐)
可以改成这样,但失去了不可变的优势
public record Person
{
public string Name { get; set; }
public int Age { get; set; }
}
底层原理(编译后)
编译器实际上生成的是一个普通类,只是自动生成了
- 构造函数
Equals、GetHashCodeToStringDeconstructwith克隆支持
这意味着也可以重写这些方法来自定义行为
and, or, not
C#9.0引入逻辑模式匹配关键字:and, or, not
它们不是用来替代&&, ||, |,而是只在模式匹配(pattern matching)上下文中生效
背景:模式逻辑表达式的自然语言化
在传统C#中
if (x > 0 && x < 10)
这是典型的逻辑运算符(operators)
当当模式匹配出现后,可以写出
if (x is > 0 and < 10)
这时候and, or, not并不是操作符,而是模式组合符
它们属于pattern syntax,而不是boolean logic
语法规则
| 关键字 | 对应符号 | 含义 | 适用场景 | ||
|---|---|---|---|---|---|
and | && | 两个模式都要匹配 | 模式组合 | ||
or | ` | ` | 任意一个模式匹配即可 | 模式选择 | |
not | ! | 匹配模式取反 | 模式否定 |
这些关键字只能出现在pattern matching表达式中(is或switch),不能在普通布尔逻辑中替代符号形式
and——逻辑与(组合两个模式)
int x = 7;
if (x is > 0 and < 10)
Console.WriteLine("在0~10之间");
等价于
if (x > 0 && x < 10)
但and的强大之处在于:它可以组合任意两种“模式”,不仅限于数值比较
object obj = 42;
if (obj is int n and > 0)
Console.WriteLine($"正整数 {n}");
int n是一个类型模式> 0是一个关系模式and把它们组合成“是int且大于0”的模式
or——逻辑或(匹配任意一个模式)
int x = 0;
if (x is < 0 or > 100)
Console.WriteLine("超出范围");
等价于
if (x < 0 || x > 100)
同样可以组合不同类型的模式
object obj = "Hello";
if (obj is int or string)
Console.WriteLine("是数字或字符串");
not——模式取反
object obj = null;
if (obj is not null)
Console.WriteLine("不是 null");
等价于
if (obj != null)
也可以取反整个复杂模式
if (x is not (>0 and < 10))
Console.WriteLine("不在 0~10 之间");
C#逐步在向“模式导向语言”转型。模式匹配的语法希望更自然、可读、接近英语表达
is/as
is和as是两个用于类型转换和类型检查的关键字
is
is用来检查对象是否是某个类型的实例,它返回一个布尔值,表示指定对象是否能够成功转换为目标类型
object obj = "Hello, world";
bool result = obj is string; // true, typeof(obj) == string
- 类型检查:
is可以检查对象是否是指定类型,或者是否可以隐式转换为目标类型 - 返回值:如果对象符合目标类型,则返回
true,否则返回false
C#7.0之后,is可以同时进行类型检查和类型转换,这被称为模式匹配。如果类型匹配,obj会自动转换为目标类型,如果不匹配,不会进行转换
object obj = "Hello, world!";
if (obj is string str) Console.WriteLine(str);
as
as是安全的转换,将对象转换为指定的目标类型,as在转换时不会抛出异常,如果转换成功,返回目标类型的对象;如果失败,返回null
object obj = "Hello, world";
string str = obj as string; // str 是 ”Hello, world“
as用于将对象转换为目标类型,并且如果转换失败返回null,这对引用类型非常有用- 无法转换的情况下,
as返回null(而不是抛出异常)
对于值类型,如int、struct等,as不能用于值类型转换,如果尝试将一个值类型转换成另一个值类型,as将无法工作,编译时会报错
float f1 = 2.3f;
double d1 = 3.33;
double d2 = f1 as double;
float f2 = d1 as float; // 编译错误
为什么as对值类型没有作用as 在底层实际上执行的是 引用类型的安全转换。它试图将一个对象引用转换为目标类型,如果目标类型不匹配,它会返回 null。但对于值类型,as 没有办法做这种转换,因为值类型本身没有像引用类型那样的 类型兼容性
is vs as
| 特性 | is 关键字 | as 关键字 |
|---|---|---|
| 功能 | 类型检查,并且可以做类型转换(模式匹配) | 安全的类型转换,如果失败返回 null |
| 返回值 | true 或 false(布尔值) | 转换后的目标类型(或 null) |
| 转换失败时行为 | 如果转换失败,返回 false | 转换失败时返回 null |
| 使用场景 | 用于检查对象是否符合某类型,适用于类型匹配检查 | 用于安全的类型转换,避免异常抛出 |
| 适用类型 | 适用于所有类型(包括值类型和引用类型) | 只适用于引用类型(class、interface,不能用于值类型) |
| 性能 | 性能稍差,进行类型检查 | 性能较好,不会抛出异常(适合频繁调用) |
- 类型检查与转换
is用来检查对象是否是某个类型的实例,并且可以在类型匹配时进行转换as用来尝试将对象转换为指定类型,如果转换失败,返回null
- 适用场景
is更适用于你只关心类型是否匹配的场景as更适用于你不确定类型转换是否会成功的场景,避免异常抛出
- 性能差异
is在检查类型时,可能需要更复杂的检查(特别时类型匹配时)as通常比is性能好,因为它直接尝试转换,而不会检查类型后转换
使用场景示例
场景1:类型检查与转换is
假设你有一个动物类Animal和它的派生类Dog,你想检查一个Animal对象是否是Dog类型
class Animal { }
class Dog : Animal { }
Animal animal = new Dog();
if (animal is Dog dog)
Console.WriteLine("dog");
这里的 is 既是类型检查,又进行了类型转换,animal 被转换为 Dog 类型,赋值给变量 dog
场景2:安全的类型转换as
假设你有一个对象,它可能是某个类型,也可能不是,你希望安全地转换它
object obj = "Hello, world!";
string str = obj as string;
if (str != null) Console.WriteLine("success");
else Console.WriteLine("default");
跳转指令
break- 作用:立即退出当前循环(
for/while/do while/switch)
- 作用:立即退出当前循环(
for (int i = 0; i < 10; i++)
{
if (i == 5)
break; // 当 i == 5 时退出循环
Console.WriteLine(i);
}
// 输出 0, 1, 2, 3, 4
- 只会跳出当前层级的循环
- 常用于提前结束循环,比如找到目标后不再继续搜索
continue- 作用:跳过当前这次循环,直接进入下一次循环迭代
for (int i = 0; i < 5; ++i)
{
if (i == 2)
continue; // 跳过 i == 2 这次循环
Console.WriteLine(i);
}
// 输出 0, 1, 3, 4
- `continue`不会终止循环,只是“跳过这次”
- 通常用于“跳过不需要处理的情况”
goto- 作用:跳转到代码中带有特定标签的位置
int x = 0;
start: // 标签
Console.WriteLine(x);
x++;
if (x < 3)
goto start; // 跳回标签处
// 输出 0, 1, 2
- `goto`是“无条件跳转”,强大但危险
- 会破坏结构化编程的逻辑流,不建议随便用
- 唯一合理使用场景:
- 从嵌套循环中跳出多层;
- 或在`switch`中做复杂分支跳转
return- 作用:立即结束当前方法的执行,并可选择性地返回一个值 无返回值
void PrintNumbeer(int n)
{
if (n < 0)
return; // 提前结束函数
Console.WriteLine(n);
}
有返回值
int Add(int a, int b)
{
return a + b;
}
return会立刻离开当前函数- 常用于错误检测、提前退出或返回结果
yield return
yield return是C#提供的一种语法糖,用于快速、方便地创建迭代器
- 目的:能轻松编写一个方法,该方法可以按需返回一个序列(如集合)中的元素,而不是一次性返回整个集合
- 关键字:
yield return,yield break - 返回值类型:使用
yield return的方法的返回值必须是IEnumerable,IEnumerable<T>,IEnumerator或IEnumerator<T>
简单来说,它让你能像写普通方法一样,写一个能“记住”执行状态的方法,每次调用只返回下一个值
执行模型
当写一个含有yield return的方法
IEnumerable<int> Foo() {
yield return 1;
yield return 2;
yield return 3;
}
编译器不会按字面理解那样生成一个返回值列表
编译器会偷偷把这个方法编译成一个隐藏类,这个类实现了IEnumerable<T>和IEnumerator<T>
也就是说,编译器自动实现了
- 一个状态机类
- 一个记录当前运行进度的字段(如state = 0, 1, 2…)
MoveNext()方法中用switch(state)控制流程Current()属性返回当前值
大致的结构(由编译器自动生成)
private sealed class FooIterator : IEnumerable<int>, IEnumerator<int>
{
private int _state;
private int _current;
public bool MoveNext()
{
switch (_state)
{
case 0:
_state = 1;
_current = 1;
return true;
case 1:
_state = 2;
_current = 2;
return true;
case 2:
_state = 3;
_current = 3;
return true;
}
return false;
}
public int Current => _current;
}
就像Unity的Coroutine也是用C#的状态机做出来的一样
写一行yield return,实际底层是编译器在创建一个小型“协程”
执行流程
当写下
foreach (var x in Foo())
{
...
}
流程是这样的
- 调用
Foo(),返回的是一个“迭代器对象”(状态机实例) - foreach调用它的
MoveNext() - 方法运行到第一个
yield return—— 暂停!返回元素 - 下一次
MoveNext(),从暂停位置继续执行 - 再次遇到
yield return—— 再暂停 - 直到代码跑到结尾
这就是“能暂停的函数”;把普通函数变成可挂起/可恢复的流程流程
yield return != return
这完全是两个东西
return方法结束yield return方法暂停,等下回继续
这种能力是在C++里必须手写状态机、lambda或coroutine才能实现
示例
- 在循环中求值
static IEnumerable<int> GenerateEvenNumbers(int count)
{
for (int i = 0; i < count; i++)
{
// 每次返回一个值
yield return i * 2;
}
}
// 使用
foreach (int even in GenerateEvenNumbers(5))
{
Console.WriteLine(even); // 输出 0, 2, 4, 6, 8
}
- 使用
yield break提前终止
static IEnumerable<string> GetMessageUntilStop(string[] message)
{
foreach (string msg in message)
{
if (msg == "stop")
{
yield break; // 立即终止
}
yield return msg;
}
}
yield return的存在意义与适用场景
有些数据没必要一次性构造全部
比如读大文件
IEnumerable<string> ReadLines(string path)
{
using var r = new StreamReader(path);
stirng line;
while ((line = r.ReadLine()) != null)
{
yield return line;
}
}
传统写法会
- 把所有行读进数组
- 占用内存
- 等待文件全部读完才能用
yield return则是
- 读到一行 -> 立刻往外产出
- 外部消费一行 -> 继续读下一行
- 内存占用极小
适用场景
- 惰性求值
这是
yield return最重要得特性。元素只在被请求时(即foreach循环到它时)才生成。这在处理大量数据或无限序列时极其高效,因为它不需要一次性将所有数据加载到内存中 - 大数据流处理
- 图遍历(DFS/BFS)
- 编写解析器(Yield-based tokenizer)
- 组合复杂迭代逻辑(比如Unity的协程) 比如根据角色状态生成下一帧行为
IEnumerable<Action> BehaviorTreeNode()
{
yield return Patrol();
yield return Chase();
yield return Attack();
}
注意事项和常见误区
- 多次迭代 每次遍历迭代器方法,都会创建一个新的状态机实例,导致方法从头开始执行
var numvers = GetNumbers(); // 这只是一个IEnumerable<int> ,不是具体的数据
foreach (var n in numbers) { ... } // 执行一次 GetNumbers 方法
foreach (var n in numbers) { ... } // 再次执行 GetNumbers 方法,从头开始
如果方法内部有昂贵的操作(如数据库查询),这会导致性能问题。解决方法:如果确定需要多次遍历,可以将其具体化,例如通过.ToList()或.ToArray()
var numbersList = GetNumbers().ToList(); // 立即执行方法,将所有结果存入List
不支持所有接口 返回值类型必须是前面提到的四种接口之一
IDisposable编译器生成的状态机实现了IDisposable。如果使用手动枚举器,最好使用using语句
using (var enumerator = GetNumbers().GetEnumerator())
{
while(enumerator.MoveNext())
{
...
}
}
(在foreach中,编译器会自动处理这些)
- 线程安全 编译器生成的状态机不是线程安全的。多个线程同时遍历同一个迭代器实例会导致不可预知的行为
[]
C#中的下标访问只是语法糖
something[index]
会被编译成
something.get_Item(index)
写入
something[index] = value;
等价于
something.set_Item(index, value);
这件事依赖于两个条件
- 类型具有名为
Item的成员(通常是索引器) - 类型标记了
[DefaultMember("Item")]
只要满足这两点,C#编译器才能识别这个类型有“默认索引器”
[DefaultMember("Item")]
[DefaultMember("Item")]是个很隐蔽但很关键的老属性,它的作用是告诉编译器:这个类型的“默认成员”叫Item
换句话说,它让C#支持
obj[x]
这种下标访问语法
没有这个属性,编译器不知道[]应该绑定哪个成员函数\
Item
Item是索引器的真实名字,当你使用索引器时,其实在访问Item
C#把Item包装成一个“类数组的语法糖”:obj[x]本质是obj.Item[x]的语法糖
.NET BCL选择了一个通用名字Item,确保所有实现索引器的类型都能用同一套编译规则
这是设计者的取舍:强制统一名字,才能在语言层保证索引器语法是固定的\
存在意义
一切都来自CLR的世界比较古早的设计
在.NET初期,CLR并不知道C#所谓的“索引器”概念——在IL层面,它只是一个名字叫Item的属性
为了让C#编译器在看到obj[]语法后知道去找“Item属性”,就得告诉它:这个类的默认成员就是Item,于是有了这个属性
在现代C#中
因为现代C#语法糖越来越高级,实际写索引器时是这样
public class MyList<T>
{
public T this[int index] => ...;
}
编译器自动生成
[DefaultMember("Item")]
public class MyList<T> { ... }
没有写它,编译器也会自动补充上
只有在
- 用反射
- 自己生成IL
- 做CodeDOM
- 写动态类型桥接层
- 分析第三方编译器产生的代码
- 搞C#和其他语言互操作(比如COM或IronPython)
时才会看到它
XML注释
XML注释(也称为文档注释)在C#中是用来生成程序文档的工具,格式类似于XML,用于为类、方法、属性、字段等成员提供详细的说明。它不仅有助于代码的可读性,还能够通过工具生成API文档,方便其他开发者理解你的代码
基本结构
XML注释的基本格式如下
/// <summary>
/// 这是对成员的简短描述
/// </summary>
这种注释格式通常出现在方法、属性、类等成员的上方。<summary>标签用于提供简短的描述,描述该成员的作用
常见的XML注释标签
<summary>:简要说明一个成员的作用或功能
/// <summary>
/// 用户管理类
/// </summary>
<param>:描述方法参数,通常用于方法和构造函数。name属性指定参数的名称
/// <param name="id">
/// 用户ID
/// </param>
<returns>:说明方法返回值的含义
/// <returns>
/// 操作是否成功
/// </returns>
<remarks>:提供额外的说明信息,通常用于描述实现细节
/// <remarks>
/// 这个方法线程安全
/// </remarks>
<exception>:描述方法可能抛出异常
/// <exception cref="ArgumentNullException">
/// 参数为null时抛出
/// </exception>
<example>:给出如何使用该方法、类或属性的示例
/// <example>
/// 见代码示例
/// </example>
<value>:用于描述属性的值<typeparam>:泛型类型参数说明<seealso>:参考其他相关主题<code>:内嵌代码<para>:段落格式(在<remarks>中使用)
示例
<remarks>用于补充详细信息,通常在<summary>之后
/// <summary>
/// 计算两个数的商
/// </summary>
/// <param name="a">被除数</param>
/// <param name="b">除数</param>
/// <returns>返回值</returns>
/// <remarks>如果除数为0,将抛出除零异常</remarks>
public double Divide(double a, double b)
{
if (b == 0)
throw new DivideByZeroException();
return a / b;
}
<exception>用于描述方法可能抛出的异常
/// <summary>
/// 从文件中读取数据
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns>返回文件内容</returns>
/// <exception cref="FileNotFoundException">文件未找到时抛出</exception>
/// <exception cref="IOException">读取过程中发生 IO 错误时抛出</exception>
public string ReadFile(string filePath)
{
if (!File.Exist(filePath))
throw new FileNotFoundException("文件未找到", filePath);
return File.ReadAllText(filePath);
}
- 一个完整示例
/// <summary>
/// 表示一个用户实体
/// </summary>
/// <remarks>
/// <para>这个类用于存储用户的基本信息</para>
/// <para>包含用户ID、姓名和年龄</para>
/// </remarks>
public class User
{
/// <summary>
/// 用户ID
/// </summary>
/// <value>正整数,唯一标识用户</value>
public int Id { get; set; }
/// <summary>
/// 用户姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 根据ID查找用户
/// </summary>
/// <param name="id">要查找的用户ID</param>
/// <returns>找到的用户对象,未找到时返回null</returns>
/// <exception cref="ArgumentException">当id小于等于0时抛出</exception>
/// <example>
/// <code>
/// var user = User.FindById(1);
/// if (user != null)
/// {
/// Console.WriteLine(user.Name);
/// }
/// </code>
/// </example>
/// <seealso cref="GetAllUsers"/>
public static User FindById(int id)
{
if (id <= 0)
throw new ArgumentException("ID必须大于0", nameof(id));
// 实际查找逻辑
return null;
}
/// <summary>
/// 获取所有用户
/// </summary>
/// <typeparam name="T">返回的集合类型</typeparam>
/// <returns>用户列表</returns>
public List<User> GetAllUsers<T>() where T : class
{
return new List<User>();
}
}
- 引用其他成员
/// <summary>
/// 使用<see cref="CalculateTotal"/>方法计算结果
/// 参考<seealse cref="MathHelper"/>
/// </summary>
- 条件注释
/// <summary>
/// 异步获取数据
/// </summary>
/// <include file='ExtraComments.xml' path='docs/members[@name="AsyncMethod"]/*'>
IDE支持和智能感知
注意事项
- XML必须格式良好:标签必须正确闭合
- cref属性值必须有效:引用的类型必须存在
- 注释与实际代码一致:避免误导
- 不要过度注释:自解释的代码不需要过多注释
使用XML的好处
自动化文档生成:通过XML注释,可以使用工具如
Doxygen或Visual Studio的文档生成工具来自动生成API文档- 项目配置
在.csproj文件中添加
<PropertyGroup> <GenerateDocumentationFile>true</GenerateDocumentationFile> <DocumentationFile>bin\Debug\net8.0\MyProject.xml</DocumentationFile> </PropertyGroup> - 编译输出
编译后生成XML文档文件,可以与以下工具配合
- Sandcastle:生成CHM/网站文档
- DocFX:生成现代Web文档
- Swagger:API文档(配合Swashbuckle)
- 项目配置
在.csproj文件中添加
IDE支持:许多IDE(如Visual Studio)能够解析这些注释,提供代码提示和文档预览,增强开发体验
- 智能感知提示:鼠标悬停时显示XML注释
- 快速文档视图:Ctrl+Q查看完整文档
- 自动生成:输入
///在方法/类上方自动生成模板/// <summary> /// /// </summary> /// <param name="param1"></param> /// <returns></returns>
提升代码可读性:XML注释可以帮助开发者更清晰地理解代码的意图和用法,特别是在大型项目中,文档化的代码易于维护
标准化:XML注释使得代码文档化过程更规范,便于团队协作,确保文档内容完整