外观
深入理解CSharp
进化史
C#1.0
- 面对对象基本特性:类、接口、继承、多态
- 值类型与引用类型
- 只读属性
- 弱类型集合
- 委托
C#2.0
- 泛型
- 强类型集合
- 可为空类型
int? - 迭代器
yield return/yield break - 外部别名
C#3.0
- 自动属性、简单初始化
public string Name{get;set;} - 隐式类型
var - 扩展方法
- Lambda表达式
- Linq (Language Integrated Query)
C#4.0
- 动态类型(
dynamic) - 命名实参、可选参数
Funct(Name:"123",Age:1);
C#5.0
- 异步函数 await/async
委托 delegate
类似函数指针
委托提供了简洁方法,不需要直接指定一个需要执行的行为,而是将这个行为用某种方式包含在一个对象中,这个对象可以向其他对象那样使用。
可以将委托看成一个定义了一个方法的接口,委托的实例就是在实现那个接口
构成
- 声明委托类型
delegate void StringProcessor(string input); - 委托实例方法 具有和委托类型相同的函数 (参数、返回值)
- 创建委托
StringProcessor proc1;
proc1 = new StringProcessor(PrintStr);- 调用委托
proc1("aa")->proc1.invoke("aa")//编译后的->PrintStr("aa")//实际执行
值类型引用类型
值类型:
- 枚举
- 结构 引用类型:
- 字符串
- 类
- 数组
- 委托
- 接口
外部别名
当同一个程序集中引用了两个(或多个)版本完全相同的命名空间 + 相同的类型名,但它们来自不同的程序集时可以使用
假设项目里同时引用了:
- Newtonsoft.Json 12.0.3(旧版)
- Newtonsoft.Json 13.0.3(新版) 两个版本里都有 Newtonsoft.Json.JsonConvert 这个类。 普通写法会直接冲突,编译报错。
解决办法:使用 extern alias
- 在项目文件(.csproj)里给两个引用起别名
<ItemGroup>
<!-- 旧版 -->
<Reference Include="Newtonsoft.Json">
<HintPath>..\lib\Newtonsoft.Json.12.0.3.dll</HintPath>
<Aliases>Json12</Aliases>
</Reference>
<!-- 新版 -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3">
<Aliases>Json13</Aliases>
</PackageReference>
</ItemGroup>(如果是 .NET SDK 风格项目,PackageReference 也可以加 Aliases 属性)
- 在代码文件顶部声明外部别名
extern alias Json12;
extern alias Json13;
using Json12::Newtonsoft.Json;
using Json13::Newtonsoft.Json;
// 现在可以明确区分
var oldSerializer = new Json12::JsonSerializer(); // 用 12.0.3 版本
var newSerializer = new Json13::JsonSerializer(); // 用 13.0.3 版本
string oldWay = Json12::JsonConvert.SerializeObject(obj);
string newWay = Json13::JsonConvert.SerializeObject(obj);泛型约束
确保只接受指定的引用类型或值类型
引用类型约束
实参是引用类型,必须是类型参数的第一个约束 用
T : class表示
sturct RefSample<T> where T : class
示例:
RefSample<IDisposable>RefSample<string>RefSample<int>
反例
RefSample<Guid>RefSample<int>
值类型约束
确保使用的类型实参是值类型,包括枚举
T : struct
class ValSample<T> where T : sturct
示例:
ValSample<int>
反例
ValSample<object>
构造函数类型约束
检查类型实参是否有一个可以用于创建类型示例的无参构造函数 适用于所有值类型 必须是所有类型的最后一个约束 T : new()
class Sample<T> where T : new() //T必须有公共的无参构造函数
转换类型约束
约束为你指定的一个类型
class Sample<T> where T : Stream //T必须是Stream类或Stream的子类class Sample<T> where T : U //T必须是U的子类或实现类class Sample<T> where T : IComparable<T> //T必须实现IComparable接口
组合约束
就是上面的约束组合使用
有效:
class Sample<T> where T: class, IDisposable, new()class Sample<T> where T: struct, IDisposableclass Sample<T,U> where T: class where U: struct, Tclass Sample<T,U> where T: Stream where U : IDisposable
无效:
class Sample<T> where T : class, structclass Sample<T> where T: Stream, classclass Sample<T> where T : new(), Streamclass Sample<T> where T: IDisposable, Streamclass Sample<T> where T:XmlReader, Icomparable, Icomparableclass Sample<T,U> where T: struct where U: class, Tclass Sample<T,U> where T: Stream, U: IDisposable
迭代器
迭代器是一种行为模式的范例。
行为模式是一种简化对象之间通讯的涉及模式,这是一种非常易于理解和使用的模式。它允许你访问一个数据集合中的所有元素,不用关心数据类型。能非常有效的构建出一个数据管道,经过一些列不同的转换过滤后从管道的另一头出来。
这也是Linq的核心模式之一
迭代器主要通过IEnumerator和IEnumerable接口来封装的。 如果某个类型实现了IEnumerable 就意味着它可以被迭代访问 调用GetEnumerator方法将放回IEnumerator的实现,就是迭代器本身。
使用嵌套类实现集合迭代器:
public class MyNumbers : IEnumerable<int>
{
private readonly int[] _data = { 10, 20, 30, 40, 50 };
public IEnumerator<int> GetEnumerator()
{
return new MyNumbersEnumerator(this);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// 嵌套的枚举器类(状态机)
private class MyNumbersEnumerator : IEnumerator<int>
{
private readonly MyNumbers _parent;
private int _index = -1; // 注意:通常从 -1 开始!
public MyNumbersEnumerator(MyNumbers parent)
{
_parent = parent;
}
public int Current
{
get
{
if (_index < 0 || _index >= _parent._data.Length)
throw new InvalidOperationException("枚举器位置无效");
return _parent._data[_index];
}
}
object IEnumerator.Current => Current;
public bool MoveNext()
{
_index++;
return _index < _parent._data.Length;
}
public void Reset()
{
_index = -1; // 或者 throw new NotSupportedException();
}
public void Dispose()
{
// 如果有需要释放的资源(如文件、数据库连接)放这里
}
}
}使用 yield return 简化迭代器
如果使用yield return来实现:
public IEnumerator GetEnumerator()
{
for(int index=0;index < values.Length; index++)
{
yield return values[(index + startingPoint) % values.Length];
}
}原理: 看似写了一个顺序执行的函数,但是实际上是请求编译器创建了一个状态机;编译器看到迭代块时,会为状态机创建一个嵌套类型,来记录块中的位置以及局部变量包括参数的值。
在常规函数中,return具有两个作用:返回值与中止函数的执行并且在退出时执行finally。
yield return语句具有临时退出函数的作用,直到再次调用MoveNext后又继续执行,在其中好像没有检查finally代码块的行为?实际发生了些什么
- 正常来说在离开相关作用域时,会执行finally代码块。 迭代器和普通函数不一样
yield return是暂停函数,并没有退出所以不会执行finally。(结合yield break能做到)
yield return 应用:
//简单的读取文本文件的函数
static IEnumerable<string> ReadLines(Func<TextReader> provider)
{
using(TextReader reader = provider())
{
string line;
while((line = reader.ReadLine())!=null)
{
yield return line;
}
}
}
static IEnumerable<string> ReadLines(string filename) => return ReadLines(filename,Encode.UTF8);
static IEnumerable<string> ReadLines(string filename,Encoding encoding)
{
return ReadLines(delegate { return File.OpenText(filename, encoding); })
}
//其中使用了泛型、匿名函数、迭代器块
//做到了只有在需要的时候才去获取资源,随时处于IDisposable的上下文中,随时剋释放资源
//每次都会创建独立的RextReader 不会存在占用的问题使用场景
// 场景1:只读前 10 行(非常省资源)
foreach (var line in ReadLines("very-large.log"))
{
Console.WriteLine(line);
if (++count >= 10) break; // 中途 break,文件会立刻关闭
}
// 场景2:过滤 + 转换(不把整个文件读进内存)
var errorLines = ReadLines("app.log")
.Where(line => line.Contains("ERROR"))
.Take(100);
// 场景3:LINQ 链式操作
var users = ReadLines("users.csv", Encoding.UTF8)
.Skip(1) // 跳过标题行
.Select(line => line.Split(','))
.Select(parts => new User(parts[0], parts[1]));用 Func<TextReader> + using + yield return 三者组合,实现了:
- 延迟打开文件(第一次迭代才打开)
- 正确释放资源(即使中途 break 或异常)
- 惰性读取(不把整个大文件一次性读进内存)
- 接口友好(返回 IEnumerable<string>,可直接 foreach / LINQ) .NET 中处理大文件行读取的经典、优雅、安全写法之一,几乎所有现代的文件读取工具类(包括 .NET 源码中的一些 helper)都采用类似结构。
Linq
查询表达式
- 序列
- 执行表达式时 是先获取结果在返回时过滤或者其他处理
var result = from person in people where person.Age >= 18 select person.Name //调用者 -> select -> where -> list(people) -> where(开始过滤Age>=18) -> select(Name) -> 拿到结果
- 执行表达式时 是先获取结果在返回时过滤或者其他处理
提示
从表达式的写法就能看出来他的执行顺序 from ... where ... select,而不是像sql一样。因为是从数据源开始,where过滤,select投影
延迟查询
- 查询表达式被创建时,不会处理任何数据,而是在内存中生成这个查询的表现形式,直到访问结果的元素时 才开始使用
let子句
- 简单的理解为中间变量
Lambda
匿名函数演化:
转换为Lambda表达式
delegate(String text) { return text.Length; }单个表达式,无括号
(String text) => { return text.Length; }让编译器推断参数类型
(String text) => text.Length去除不必要的括号
text => text.Length
表达式树
// 手动构建 x => x + 3 * 2
var parameter = Expression.Parameter(typeof(int), "x");
var constant3 = Expression.Constant(3);
var constant2 = Expression.Constant(2);
var multiply = Expression.Multiply(constant3, constant2); // 3*2
var add = Expression.Add(parameter, multiply); // x + (3*2)
var lambda = Expression.Lambda<Func<int, int>>(add, parameter);
var func = lambda.Compile();
Console.WriteLine(func(10)); // 输出 16扩展方法
特征
- 在非嵌套、非泛型的静态类(所以必须是静态方法)
- 至少要有一个参数
- 第一个参数必须附加
this关键字 - 第一个参数不能有任何其他修饰符 ref\out之类的
- 第一个参数不能是指针类型
