内容:
第四章(上): Advanced C#
- Delegate
- Events
- Lamda
- Anonymous Methods
- Exception Handler
- Enumeration & Iterartor
- Nullable Types
- Extension Methods
- Anonymous Types
1. Delegate
一个delegate type定义了 delegate 实例可以调用的方法类型。
例子:
// 这里定义了一个delegate类型,规定输入输出的参数类型
delegate int Transformer (int x);
class Test
{
static void Main()
{
//Transformer t = new Transformer (Square);
Transformer t = Square; // Create delegate instance
//t.Invoke(3)
int result = t(3); // Invoke delegate
Console.WriteLine (result); // 9
}
static int Square (int x) => x * x;
}
1.1 Writing Plug-in Methods with Delegates
就是把委托当做函数的参数, 这个函数就叫做高阶函数: high-order function
1.2 Multicast Delegates☆
每个delegate instance可以引用不止一个target method:
SomeDelegate d = SomeMethod1;
d += SomeMethod2;
注意:
- 调用delegate instance的顺序,会和 添加引用顺序一致;
- delegate 是不可变的,当使用
+=
和-=
的时候,实际上是创建了一个新的delegate instance,再给他赋值 - 如果被代理的method有返回值,那么调用delegate得到的返回值为最后执行的method的返回值
- 所有delegate继承自
System.MulticastDelegate
,再继承自System.Delegate
- 使用
+=
和-=
实际上会被编译成System.Delegate
的静态方法:Combine
和Remove
1.3 实例目标方法 vs. 静态目标方法
当一个delegate instance 引用了某个实例 target method的时, System.Delegate
类的Target
属性维护了这个方法的实例对象, Method
属性指向了这个方法。
class Test
{
static void Main()
{
X x = new X();
ProgressReporter p = x.InstanceProgress;
p(99); // 99
Console.WriteLine (p.Target == x); // True
Console.WriteLine (p.Method); // Void InstanceProgress(Int32)
}
}
class X
{
public void InstanceProgress (int percentComplete) => Console.WriteLine (percentComplete);
}
1.4 泛型委托
定义委托类型的时候可以使用泛型
public delegate T Transformer<T> (T arg);
1.5 Func & Action
除了 ref/out
和 pointer 参数
的 delegate,其他基本都可以用这两个替换
1.6 Delegate vs. Interface
用delegate解决的地方也能使用接口解决,那么何时使用delegate更好:
- 接口本身只定义了一个方法
- 需要multicast delegate
- subscriber需要多次实现接口
1.7 Delegate兼容性
a. 类型兼容性
delegate类型都彼此不兼容,就算他们 函数签名相同:
delegate void D1();
delegate void D2();
...
D1 d1 = Method1;
D2 d2 = d1; // Compile-time error
// 但是下面这个可以
D2 d2 = new D2 (d1);
Delegate相等的条件是,他们的target methods相等:
D d1 = Method1;
D d2 = Method1;
Console.WriteLine (d1 == d2); // True
Multicast delegates are considered equal if they reference the same methods in the same order.
b. 参数兼容性
delegate的参数具有 contravariance: (比如 传object参数的方法 给 string参数的delegate)
static void ActOnObject (object o) => Console.WriteLine (o);
...
delegate void StringAction (string s);
StringAction sa = new StringAction (ActOnObject);
c. 返回值兼容性:
delegate的参数具有 covariance: (比如 传string返回值的方法 给 object返回值的delegate)
static string RetrieveString() => "hello";
...
delegate object ObjectRetriever();
ObjectRetriever o = new ObjectRetriever (RetrieveString);
d. 泛型delegate使用variance
如果定义泛型的delegate,好的习惯是:
- 把作为返回值的type parameter表示为
out
- 把作为参数传入的 type parameter 表示为
int
2. Events
目的:
使用delegate主要是为了实现broadcaster-subscriber模式。 broadcaster内有一个 delegate instance, subscriber通过向这个delegate instance注册target method,来接收消息;broadcaster来决定何时invoke这个delegate。
Events作用:
event是C#提供的实现这个模式的语法, 它相当于是 delegate的一个子集,只提供了完成 subscriber模式必须的功能。event的存在是为了阻止 subscriber间的相互干扰。
// Delegate definition
public delegate void PriceChangedHandler (decimal oldPrice,
decimal newPrice);
public class Broadcaster
{
// Event declaration
public event PriceChangedHandler PriceChanged;
}
在上面的 Broadcaster
类中,可以像对待delegate一样对待这个event; 而在这个类的外面,只能使用+=
和-=
。
好处☆:
虽然没有event
,代码也同样执行,但是event能提供如下的安全性:
- 让其他的subscriber没有办法重新赋值
PricedChanged
,而只能使用+=
- 没法让
event
清除所有的subscriber - 没办法从外部调用event
2.1 Event内部怎么工作的
public class Broadcaster
{
public event PriceChangedHandler PriceChanged;
}
compiler会把上面的,转换成类似下面这样:
PriceChangedHandler priceChanged; // private delegate
public event PriceChangedHandler PriceChanged
{
add { priceChanged += value; }
remove { priceChanged -= value; }
}
add
和remove
关键字,就相当于属性的一样,控制访问。别的类使用+=
和-=
的时候,实际上是调用了add
和remove
,且只能使用这两种方式来访问。
2.2 Standard Event Pattern
C#有一个标准的 模式来使用event( 使用 System.EventArgs
):
1.定义自己的 EventArgs,继承自 System.EventArgs
,在其中保存event需要传递的信息:
public class PriceChangedEventArgs : System.EventArgs
{
//保存的信息,通常是 只读属性或者read-only field
public readonly decimal LastPrice;
public readonly decimal NewPrice;
//构造器, 在其中保存2个要传递的信息
public PriceChangedEventArgs (decimal lastPrice, decimal newPrice)
{
LastPrice = lastPrice;
NewPrice = newPrice;
}
}
2.定义一个delegate event的类型
三个原则:
1. 返回值为void
2. 接收2个参数,第一个是object,代表event broadcaster对象; 第二个参数是 EventArgs的子类,代表消息
3. 名字必须以`EventHandler`结尾
推荐: 使用框架定义的泛型类: System.EventHandler<>
来创建:
public delegate void EventHandler<TEventArgs>
(object source, TEventArgs e) where TEventArgs : EventArgs;
在有泛型以前,只能自定义一个 EventHandler类型;因为历史原因,所以现在很多见到的EventHandler都是以前自定义的
3.定义event实例:
public class Stock
{
...
public event EventHandler<PriceChangedEventArgs> PriceChanged;
}
4.创建 protected virtual
方法,在其中fire event:
创建的这个函数名必须以On
开头,之后加上这个Event Instance的名字
public class Stock
{
...
public event EventHandler<PriceChangedEventArgs> PriceChanged;
protected virtual void OnPriceChanged (PriceChangedEventArgs e)
{
if (PriceChanged != null) PriceChanged (this, e);
}
}
5.对于多线程安全的情况,需要把delegate给一个临时变量,使用这个临时变量来invoke delegate:
var temp = PriceChanged;
if (temp != null) temp (this, e);
6.对于不需要传递信息的EventArgs
,可以直接使用EventArgs.Empty
来返回一个实例:
protected virtual void OnPriceChanged (EventArgs e)
{
PriceChanged?.Invoke (this, e);
}
2.3 Event Accessors
普通的定义一个event:
public event EventHandler PriceChanged;
会被编译器处理为:
- 一个私有的delegate field
- 一组公开的 event accessor functions:
add_PriceChanged
和remove_PriceChanged
所以我们可以自定义这么一个event accessor:
private EventHandler priceChanged; // Declare a private delegate
public event EventHandler PriceChanged
{
add { priceChanged += value; }
remove { priceChanged -= value; }
}
但是C#自身会使用compare-and-swap算法来保证更新delegate时候的线程安全。
2.4 Event Modifiers
event也可以使用 virtual
,overridden
,abstract
,sealed
,static
来修饰
3. Lambda 表达式
lambda表达式是为了替换delegate的无名方法。 编译器会把lambda表达式编译成下面2者之1:
- A delegate instance
- An expression tree, of type
Expression<TDelegate>
, representing the code inside the lambda expression in a traversable object model. This allows the lambda expression to be interpreted later at runtime
a. 形式
(parameters) => expression-or-statement-block
lambda表达式的参数和返回值,会根据对应的delgate类型来推断出来。
例子:
Transformer sqr = x => x * x;
Console.WriteLine (sqr(3)); // 9
这种形式的lambda表达式,会被编译器内部编译成一个private 的方法,然后把表达式的内容放到这个方法里面。
3.1. 指定lambda 参数类型
当遇到无法推断类型的情况,需要自己指定参数类型:
Bar ((int x) => Foo (x));
3.2 Capturing Outer Variables☆
lambda表达式可以引用外部的变量:
static void Main()
{
int factor = 2;
Func<int, int> multiplier = n => n * factor;
Console.WriteLine (multiplier (3)); // 6
}
被引用的外部变量叫做 captured variable, 这种情况叫做closure(闭包)
1.captured variable是在delegate被调用的时候,才会计算值:
int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30
2.lambda表达式内部可以改变外部变量的值:
int seed = 0;
Func<int> natural = () => seed++;
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
Console.WriteLine (seed); // 2
3.外部变量的生命周期被延长到了和这个lambda表达式一样长:
static Func<int> Natural()
{
int seed = 0;
return () => seed++; // Returns a closure
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
}
4.在Lambda表达式中声明的新变量,对于每一次调用这个delegate , 这个变量都是一个新的:
static Func<int> Natural()
{
return() => { int seed = 0; return seed++; };
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 0
}
5.lambda内部实际上是把这个captured variable
给 hoisting到了一个private class中,当delegate被调用的时候,会初始化这个 private class,它的生命周期和这个delegate一样。
a. capturing iteration variables
当capture一个在for循环中的变量时,lambda将这个变量当做声明在这个循环之外,所以每一次循环都会capture同一个变量:
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
actions [i] = () => Console.Write (i);
foreach (Action a in actions) a(); // 333
这3个lambda表达式引用的都是同一个i
,且i
的值是在调用lambda的时候才计算:
Action[] actions = new Action[3];
int i = 0;
actions[0] = () => Console.Write (i);
i = 1;
actions[1] = () => Console.Write (i);
i = 2;
actions[2] = () => Console.Write (i);
i = 3;
foreach (Action a in actions) a(); // 333
如果想要每次的调用结果不一样,需要在lambda内部自己创建局部变量来记录:
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
int loopScopedi = i;
actions [i] = () => Console.Write (loopScopedi);
}
foreach (Action a in actions) a(); // 012
在C# 5.0以前,foreach 循环也会导致和for循环相同的结果,但是C# 5.0以后修改了,所以:
Action[] actions = new Action[3];
int i = 0;
foreach (char c in "abc")
actions [i++] = () => Console.Write (c);
foreach (Action a in actions) a(); // ccc in C# 4.0 ; abc in C# 5.0
3.3 Lambda vs. Local method
他们2个的效果是重叠的,但是局部方法有下列三个优势:
- 他们自身能递归调用自己
- 他们避免了一大堆的delegate 声明
- 引入较小的
overhead
,没有hoisting
总的来说,局部方法会更加的方便和性能高, 但是delegate会写的更简洁
4. Anonymous Methods
delegate (参数列表) {函数体};
各种性质和lambda一样
5. Try和 Exception
try
{
... // exception may get thrown within execution of this block
}
catch (ExceptionA ex)
{
... // handle exception of type ExceptionA
}
catch (ExceptionB ex)
{
... // handle exception of type ExceptionB
}
finally
{
... // cleanup code
}
5.1 catch 块
从C# 6.0开始,有了exception filter:
catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout)
{
...
}
5.2 finally 块
finally块一直会执行,除非内部之前无限循环。
对实现了System.IDisposable
的使用using
using (StreamReader reader = File.OpenText ("file.txt"))
{
...
}
相当于
{
StreamReader reader = File.OpenText ("file.txt");
try
{
...
}
finally
{
if (reader != null)
((IDisposable)reader).Dispose();
}
}
5.3 抛出异常
throw new ArgumentNullException (nameof (name));
a. C# 7.0 throw作为表达式
public string Foo() => throw new NotImplementedException();
value == null ? throw new ArgumentException ("value") : "";
b. 再次抛出异常
一般是无法处理,所以写个log什么的之后再次抛出;或者再次抛出一个更specific的异常类型:
try { ... }
catch (Exception ex)
{
// Log error
...
throw; // Rethrow same exception
}
throw new XmlException ("Invalid DateTime", ex); //ex作为了新异常的 `InnerException`
5.4 Key Properties of System.Exception
属性名 | 作用 |
---|---|
StackTrace |
A string representing all the methods that are called from the origin of the exception to the catch block |
Message |
A string with a description of the error. |
InnerException |
The inner exception (if any) that caused the outer exception. This, itself, may have another InnerException |
5.5 常见异常类型
- System.ArgumentException
- System.ArgumentNullException
- System.ArgumentOutOfRangeException
- System.InvalidOperationException
- System.NotSupportedException
- System.NotImplementedException
- System.ObjectDisposedException
- NullReferenceException :
throw null
;
·
5.6 The TryXXX Method Pattern
一个方法内部可能有异常,可以定义2个, 一个是直接抛出异常等待处理,另一个是返回调用结果,让用户之后处理:
可以在普通方法内部调用TryXXX
:
public return-type XXX (input-type input)
{
return-type returnValue;
if (!TryXXX (input, out returnValue))
throw new YYYException (...)
return returnValue;
}
6. Enumeration and Iterators
6.1 Enumeration
Enumerator是一个只读、只往前走的一系列值Cursor,Enumerator要实现下列接口之一,包含Current
属性和MoveNext
方法:
- System.Collections.IEnumerator
- System.Collections.Generic.IEnumerator
Enumerable对象可以被foreach循环访问,它本身并不是cursor,但是会产生遍历自身的cursor,自己表示了一系列数字, 要满足下列条件之一:
- 实现
IEnumerable
orIEnumerable<T>
- 有一个方法叫做
GetEnumerator
并且返回一个 Enumerator
例子:
class Enumerator // Typically implements IEnumerator or IEnumerator<T>
{
public IteratorVariableType Current { get {...} }
public bool MoveNext() {...}
}
class Enumerable // Typically implements IEnumerable or IEnumerable<T>
{
public Enumerator GetEnumerator() {...}
}
using (var enumerator = "beer".GetEnumerator()) // 之后释放 Enumerator资源
while (enumerator.MoveNext())
{
var element = enumerator.Current;
Console.WriteLine (element);
}
等同于:
foreach (char c in "beer"){...}
6.2 Collection Initializers
一个 enumerable 对象,并且有Add
方法以及适当的参数:
List<int> list = new List<int> {1, 2, 3};
相当于:
List<int> list = new List<int>();
list.Add (1);
list.Add (2);
list.Add (3);
var dict = new Dictionary<int, string>()
{
{ 5, "five" },
{ 10, "ten" }
};
var dict = new Dictionary<int, string>()
{
[3] = "three",
[10] = "ten"
};
6.3 Iterator
iterator产生enumerator:
static IEnumerable<int> Fibs (int fibCount)
{
for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
{
yield return prevFib; // 返回下一个enurator得到的值
int newFib = prevFib+curFib;
prevFib = curFib;
curFib = newFib;
}
}
编译器内部把这个编译成了一个 private class,实现了IEnumerable<T> and/or IEnumerator<T>
, 这个类的生命周期是和Enumerator绑定的,意思是当不去遍历它的时候,这个类结束。
6.4 Iterator的语法
- iterator是一个包含一条或者多条
yield
语句的 : 方法、属性 或者 indexer - 一个iterator必须返回下列4个接口之一:
// Enumerable interfaces
System.Collections.IEnumerable
System.Collections.Generic.IEnumerable<T>
// Enumerator interfaces
System.Collections.IEnumerator
System.Collections.Generic.IEnumerator<T>
a. 多条yield return
static IEnumerable<string> Foo()
{
yield return "One";
yield return "Two";
yield return "Three";
}
static void Main()
{
foreach (string s in Foo())
Console.WriteLine(s); // Prints "One","Two","Three"
}
b. yield return
static IEnumerable<string> Foo (bool breakEarly)
{
yield return "One";
yield return "Two";
if (breakEarly)
yield break; //提前退出iterator
yield return "Three";
}
c. Iterators and try/catch/finally blocks
yield return 只能放在 try-fianlly
的try中,不能放在try-catch
,try-catch-finally
的其他地方。这是因为编译器必须把iterator转换成一个拥有MoveNext, Current, and Dispose members
的普通类,那么还要异常处理的话就会增加这个转换的复杂性。
只有下面这个代码是可行的( try-finally中的try有yield)
IEnumerable<string> Foo()
{
try { yield return "One"; } // OK
finally { ... }
}
6.5 组合多个iterator:
using System;
using System.Collections.Generic;
class Test
{
static void Main()
{
foreach (int fib in EvenNumbersOnly (Fibs(6)))
Console.WriteLine (fib);
}
static IEnumerable<int> Fibs (int fibCount)
{
for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
{
yield return prevFib;
int newFib = prevFib+curFib;
prevFib = curFib;
curFib = newFib;
}
}
static IEnumerable<int> EvenNumbersOnly (IEnumerable<int> sequence)
{
foreach (int x in sequence)
if ((x % 2) == 0)
yield return x;
}
}
7. 可空类型
T? 是 System.Nullable<T>
的语法糖, 是一个轻量级不可变的结构体(值类型),只有2个字段,Value
和HasValue
public struct Nullable<T> where T : struct
{
public T Value {get;}
public bool HasValue {get;}
public T GetValueOrDefault();
public T GetValueOrDefault (T defaultValue);
...
}
7.1. 转换
int? x = 5; // implicit
int y = (int)x; // explicit 相当于调用 .Value
7.2. 拆装箱
When T? is boxed, the boxed value on the heap contains T, not T?
object o = "string";
int? x = o as int?; // 使用as
Console.WriteLine (x.HasValue); //False
7.3. 符号提升
Nullable<T>
本身没有运算操作,但是下面代码可以
int? x = 5;
int? y = 10;
bool b = x < y; // true
因为C#通过符号提升,将这些代码运用到了 底层的数据中使用,相当于:
bool b = (x.HasValue && y.HasValue) ? (x.Value < y.Value) : false;
7.4 相等操作符
符号提升后的==
,比较规则是这样的:
- 如果都是null,则相等
- 只有一个 不是null,则不等
- 如果都不是null,则比较Value
7.5 比较操作符
<= >= < >
这些操作符,在比较其中一个是Null的时候,都会返回false
7.6 其他操作符
+, −, *, /, %, &, |, ^, <<, >>, +, ++, --, !, ~
在有操作数为null时都会返回null
7.7 bool?和 & |操作符
&和|
把null当作位置值,所以
null | null // null
null | False // null
null | True // True
null & null //null
null & False //False
null & True //null
7.8 没有Nullable类型以前怎么办:
- 特殊值表示null
- 包装类+flag
8. Extension Methods
扩展方法是静态方法,第一个参数this表示扩展的类,参数类型就是这个扩展类的类型:
public static class StringHelper
{
// 扩展方法,扩展类型是 string
public static bool IsCapitalized (this string s)
{
if (string.IsNullOrEmpty(s)) return false;
return char.IsUpper (s[0]);
}
}
调用扩展方法,会被编译成一个普通的静态方法:
Console.WriteLine ("Perth".IsCapitalized());
//编译成
Console.WriteLine (StringHelper.IsCapitalized ("Perth"));
也可以对接口扩展
8.1 扩展方法的使用
调用静态方法,首先要保证这个定义扩展方法的namespace
已经被引入
8.2 调用冲突
- 实例方法永远优先于扩展方法, 如果此时要调用扩展方法,只能像调用静态方法一样调用
- 当存在2个签名相同的扩展方法时,只能通过 静态方法方式调用
- 当存在2个同名的扩展方法时, 参数更加准确的那个被调用
9. 匿名类
使用new
加上初始化器来指定属性。
var dude = new { Name = "Bob", Age = 23 };
编译器会自动编译成一个类型:
internal class AnonymousGeneratedTypeName
{
private string name; // Actual field name is irrelevant
private int age; // Actual field name is irrelevant
public AnonymousGeneratedTypeName (string name, int age)
{
this.name = name; this.age = age;
}
public string Name { get { return name; } }
public int Age { get { return age; } }
// The Equals and GetHashCode methods are overridden (see Chapter 6).
// The ToString method is also overridden.
}
...
1.属性的名称可以根据变量名的名称来推断出来:
var dude = new { Name = "Bob", Age, Age.ToString().Length };
相当于
var dude = new { Name = "Bob", Age = Age, Length = Age.ToString().Length };
2.同一个namespace中2个同属性的匿名类型,他们的类型相同:
var a1 = new { X = 2, Y = 4 };
var a2 = new { X = 2, Y = 4 };
Console.WriteLine (a1.GetType() == a2.GetType()); // True
//并且Equals方法也被重写来判断相等了
Console.WriteLine (a1 == a2); // False
Console.WriteLine (a1.Equals (a2)); // True