[关闭]
@zzy0471 2017-10-11T10:07:57.000000Z 字数 40451 阅读 2766

《CLR via C#》随手记

CLR .Net


第一章 CRL的执行模型

https://www.zybuluo.com/mdeditor#484995

第四章 类型基础

Object类中的方法

实例方法:

静态方法:

对象实例化过程

  1. 计算类型及其所有基类型的实例字段、类型对象指针和同步索引块所需的内存大小
  2. 从托管堆中分配内存,所有字节设置为0
  3. 初始化对象的类型对象指针和同步索引块
  4. 执行构造函数

“类型对象指针”下面有介绍,“同步索引块”用来锁定对象,当对象被锁定时(使用lock关键字),同步索引块会被赋值,详细情况可百度之。

is 和 as

is 和 as 永远不会抛出异常,如:

if(o is Employee)
{
    Employee e = (Employee)o;
}

可简化为:

Employee e = o as Employee;
if(e != null)
{
  //todo...
}

下面的代码比上面的代码少做一次类型判断(Employee e = (Employee)o的执行也需要做类型判断),性能上有所提高

托管代码在内存中的结构

CLR是一个面向栈内存编程的平台,局部变量,方法的调用,参数的传递依靠线程栈来实现。对象则存储在堆内存中,如下图所示:
图1
捕获.PNG-172.3kB

其中,类型对象可以视为System.Type的实例,调用实例对象的GetType方法可得实例对象的类型对象。类型对象中包含了静态字段和方法列表,实例对象中包含了实例字段,他们两者都包含了“类型对象指针”和“同步索引块”,实例对象的“类型对象指针”指向其对应的“类型对象”,而类型对象的“类型对象指针”指向System.RuntTimeType(System.Type的间接子类),System.Type本身也是个对象,他的“类型对象指针”指向自己。

第五章 基元类型、引用类型和值类型

溢出

默认的编译器是不抛出溢出异常的,可以对编译选项进行修改,或者使用关键字checked'和unchecked`,如下所示:
代码1

long number = long.MaxValue;
int intNumber = (int)number;
int uncheckIntNumber = unchecked((int)number);
int checkedIntNumber = checked((int)number); //发生运行错误

Console.WriteLine();

对加减乘法进行溢出判断和不进行判断的IL代码是不同的,不进行溢出判断的加法指令是add,进行溢出判断的加法指令是add.ovf。同样的,减法和乘法也是如此。

注意 System.Decimal和System.Numerics.BigInteger并非基元类型,它们的内部结构复杂,checkedunchecked不对其起作用,System.Decimal会引发OverflowException异常,BigInteger表示的数没有上限,可能会引发OutOfMemoryException异常

引用类型和值类型

在堆上分配对象空间,使用new关键字实例化并返回一个对象内存地址,赋值这个内存地址变量叫做“引用变量”,使用引用变量会造成一些性能上的损失,因为:

值类型包括结构和枚举(继承自System.Enum,System.Enum又继承自System.ValueType),他们都继承自System.ValueType,值类型在线程栈中分配,和引用类型不同,值类型的值不用像引用类型那样指向堆内存的对象的地址,而是存放本身值,当然,值类型也可以作为对象的字段而存在于堆内存中。请看如下示例:
代码2

class Program
{
    public static void Main()
    {
        RType r = new RType();
        r.Id = 1;

        VType v = new VType();
        v.Id = 1;

        RType r2 = r;
        VType v2 = v;

        r2.Id = 2;
        v2.Id = 2;

        Console.WriteLine("after mondify the values of r2 and v2.");
        Console.WriteLine("r.Id: " + r.Id); //2
        Console.WriteLine("v.Id: " + v.Id); //1

        Console.ReadLine();
    }        
}

class RType
{
    public int Id { get; set; }
}

struct VType
{
    public int Id { get; set; }
}

上例中,v赋值给v2,其实是在线程栈中复制了v给v2,所以改变v2的值对v1没有影响,而r赋值给r2其实是吧r2这个引用型变量赋值了r所指向的堆中的对象的地址,所以修改r2中的值,r中的值也变了,他们指向同一个内存地址。作为参数传递也是一样的,如下示例:
代码3

class Program
{
    public static void Main()
    {
        RType rt = new RType() { Id = 1};
        F(rt);

        VType vt = new VType() { Id = 1};
        F(vt);

        Console.WriteLine(rt.Id); // 100
        Console.WriteLine(vt.Id); // 1

        Console.ReadLine();
    }   

    private static void  F(RType type)
    {
        type.Id = 100;
    }

    private static void F(VType type)
    {
        type.Id = 100;
    }
}

class RType
{
    public int Id { get; set; }
}

struct VType
{
    public int Id { get; set; }
}

再看一个例子:
代码4

class Program
{
    public static void Main()
    {
        RType rt = new RType() { Id = 1};
        F(rt);

        Console.WriteLine(rt.Id); // 1

        Console.ReadLine();
    }   

    private static void  F(RType type)
    {
        type = null;
    }
}

class RType
{
    public int Id { get; set; }
}

上述代码中,F方法中虽然设置了type为null,但是Main方法中的Console.WriteLine(rt.Id)仍然输出的是1,要想rt的值再调用F方法后变成null,请使用ref关键字:
代码5

class Program
{
    public static void Main()
    {
        RType rt = new RType() { Id = 1};
        F(ref rt);

        Console.WriteLine(rt == null); // True

        Console.ReadLine();
    }   

    private static void  F(ref RType type)
    {
        type = null;
    }
}

class RType
{
    public int Id { get; set; }
}

实际上,ref和out关键字的作用是一样的,ref和out后面的变量用来保存调用函数后的返回值,只不过ref后面的变量既是参数又是返回值,而out后面的变量只是返回值,实际上在一个对象中定义两个同名同参数方法,然后把参数分别使用ref和out修饰,编译器会报错,认为这两个方法重复定义。

对象相等性和同一性

Object中的Equals方法比较的是两个对象的地址,比较的是同一性不是相等性(当然,对于Obejct来说,满足同一性的两个对象肯定是满足相等性的),所以如果需要比较两个自定义对象实例的相等性,需要重写Equals方法。重写后的Equals方法不具备了比较同一性的能力,所以Object又提供了一个静态方法ReferenceEqual用来比较两个对象的同一性,代码如下:
代码6

public static bool ReferenceEquals(object objA, object objB)
{
    return (objA == objB);
}

除此之外,Object还提供了一个的静态Equals方法,相信没多少人用过,内容如下:
代码7

public static bool Equals(object objA, object objB)
{
    return ((objA == objB) || (((objA != null) && (objB != null)) && objA.Equals(objB)));
}

静态的Equals方法意思是两对象满足同一性或者满足相等性就返回True

注意 判断两个对象慎用==,有可能被操作符重载而具备了非想象中功能,保险的方法是调用Object的静态方法ReferenceEquals

dynamic

参考动态语言,C#添加了dynamic关键字,dynamic很多地方和使用object是一个效果,在使用反射时用dyanmic关键字可以简化代码,提高效率:

代码8

class Program
{
    static void Main(string[] args)
    {
        Invoke(new TestClass());
        InvokeWithDynamic(new TestClass());

        Console.ReadLine();
    }

    private static void InvokeWithDynamic(dynamic obj)
    {
        obj.F();
    }

    static void Invoke(object obj)
    {
        Type t = obj.GetType();
        var info = t.GetMethod("F");
        info.Invoke(obj, null);
    }
}

class TestClass
{
    public void F()
    {
        Console.WriteLine("hello dynamic");
    }
}

第六章 类型和成员基础

类型的各种成员

  1. 常量
  2. 字段
  3. 实例构造函数
  4. 类型构造函数
  5. 方法
  6. 事件
  7. 属性
  8. 内部类型
  9. 操作符重载
  10. 转换操作符

类型的可见性和成员的可访问性

有元程序集

将类型加上特性InteralsVisibleTo,使得Internal成员可以被其他程序集访问

成员的可访问性

CLR术语 C#术语
Private private
Family protected
Family and Assembly 不支持
Assembly internal
Family or Assembly protected internal
Public public

如果没有给非构造函数成员添加访问修饰符,接口中定义的成员默认为public(实际上C#编译器不允许定义接口可访问性,只能是默认的public),类中定义的成员默认是private

此外,抽象类中的默认构造函数(不定义构造函数而由C#编译器生成的构造函数)的可访问性是protected,非抽象类的默认构造函数是public。静态构造函数(C#编译器不会生成默认的无参静态构造函数,这里只开发人员定义的构造函数)默认是private,且C#编译器不允许显示添加访问性修饰符,这样做可以防止开发人员调用到静态构造函数,静态构造函数由CLR调用。

C#编译器限制子类中重写父类中的成员时,访问性修饰符必须一致。而CLR则允许子类重写父类的成员访问性修饰符只能缩紧,不能扩大。

静态类

通常,将一些相关的工具方法封装在一个静态类中,观察静态类对应的IL代码,回复发现其实是一个用sealed关键字修饰的抽象类:

class private abstract auto ansi sealed beforefieldinit MyStaticClass
extends [mscorlib]System.Object

partial关键字

partial关键字可用在类、结构和接口上,用了将类、接口或接口分在多个文件中定义

Jeffrey对于类型的可见性和成员的可访问性使用的建议

第七章 常量和字段

常量

常量的值必须在编译期确定,编译后,常量值将被保存到程序集元数据中,所以,通常只有基元类型可以充当常量,非基元类型也可以,但是只能赋值为null。

字段修饰符

CLR术语 C#术语 备注
Static static
Instance 默认
InitOnly readonly
Volatile volatile 将在二十九章介绍

字段声明时同事可以(或必须)初始化,观察IL代码,这些初始化操作其实是在构造函数中进行的,在构造函数开始的地方初始化。情况如下实例:

代码9

namespace StaticClassTest
{
    class Program
    {
        private const int CON_VAR = 100;
        private static int _staticField = 100;
        private int _filed = 100;

        static void Main(string[] args)
        {
        }
    }
}

对应的IL:
代码10
.
.class private auto ansi beforefieldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig specialname rtspecialname static void .cctor() cil managed
{
.maxstack 8
L_0000: ldc.i4.s 100
L_0002: stsfld int32 StaticClassTest.Program::_staticField
L_0007: ret
}

.method public hidebysig specialname rtspecialname instance void .ctor(int32     field) cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: ldc.i4.s 100
    L_0003: stfld int32 StaticClassTest.Program::_filed
    L_0008: ldarg.0 
    L_0009: call instance void [mscorlib]System.Object::.ctor()
    L_000e: nop 
    L_000f: nop 
    L_0010: ldarg.0 
    L_0011: ldarg.1 
    L_0012: stfld int32 StaticClassTest.Program::_filed
    L_0017: nop 
    L_0018: ret 
}

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 8
    L_0000: nop 
    L_0001: ret 
}


.field private int32 _filed

.field private static int32 _staticField

.field private static literal int32 CON_VAR = int32(100)

}

第八章 方法

引用类型的实例构造函数

如果没有定义构造函数,那么C#编译器会生成一个默认的无参构造函数,反之,则不会生成默认无参构造函数。如果父类型没有无参构造函数,那么子类构造函数中必须显示调用一个父类的构造函数,否则编译通不过。

值类型的实例构造函数

结构体中的字段不必必须通过构造函数赋值,不调用构造函数的情况下各字段默认赋值0或null。结构中不能声明无参数构造函数,C#编译器会报错。

静态构造函数

静态构造函数只会执行一次,可以用来实现单例模式,类型的构造发生在JIT第一次使用到这个类型前,第二次再使用时则不会重复构造,CLR会确保多个线程同时创建一个类型对象时只有一个线程可以进行,其他线程阻塞,直到类型对象构建完毕。类型对象会在appDomain销毁时期内存变成不可达状态。

静态构造函数必须是是有的,实际上C#不允许开发人员制定可访问性修饰符,这样做可以防止开发人员调用,类型对象的够着函数是CLR负责调用的。

操作符重载

定义如op_XXX并应用特性specialname的方法即为操作符重载方法

转换操作符方法

个人觉得转换操作符方法用处不大,而且还起到迷惑代码阅读者的效果。等有机会碰到再来研究

扩展方法

关于扩展方法使用的一些规则:

除了可以给类定义扩展方法,还可以给接口、委托、枚举、结构体定义扩展方法,而且扩展方法也可以是泛型方法。如下示例是给一个委托定义扩展方法:

代码11

static class Class1
{
    public static void Ex<T>(this Action<T> a, T t)
    {
        if (a != null)
        {
            a(t);
        }
    }
}

分部方法

使用分部方法的一些规则:

第九章 参数

方法的值传递和引用传递

不适用关键字refout的参数传递方法就是值传递,反之是引用传递,所谓引用传递,是指传递的不是值本身,而是值得内存地址

对于值类型而言,值传递就是将被传递值复制一份并且作为方法的参数传递;引用传递就是传递值得地址,并不进行复制操作。对于大量的值传递方法,如果加上ref和out关键字可以改善性能。

对于引用类型而言,值传递就是将引用类型变量复制一份(该变量存放的是对象的内存地址)作为参数传递;引用传递则没有复制操作,传递给方法的就是引用变量自己。引用类型的引用传递有点绕,可以举个简单的例子:
代码12

static void Main(string[] args)
{
    String hello = "hello";
    ValueMethod(hello);
    Console.WriteLine(hello); //hello

    String word = "word";
    RefernceMethod(ref word);
    Console.WriteLine(word); //nothing

    Console.ReadLine();
}

static void ValueMethod(String v)
{
    v = null;
}

static void RefernceMethod(ref String v)
{
    v = null;
}

可变数量参数

调用参数数量可变的方法对性能有所影响(传递null除外),因为数组需要在堆内存分配,数组元素必须初始化,还需要进行垃圾回收。如果可能的话可以多写几个重载方法来代替参数数量可变方法,实际上FCL中很多方法就是这样定义的。

参数和返回值类型的设计规范

这两点很容易理解,参数越弱,可传递给方法的变量类型就越多,比较灵活;返回值类型越弱赋值这个返回值的变量的类型就越多,比较灵活。不必刻意为了最求这个规范而设计比较弱的参数和比较强的返回值,看实际情况而定吧。(这句话非原书内容,是会长的理解)

第十章 属性

元组

C#里也有类似Python的元组,如下:

代码13

static void Main(string[] args)
{
    Tuple<int> tuple1 = new Tuple<int>(5);
    Tuple<int, int> tuple2 = new Tuple<int, int>(5, 5);
    Tuple<int, int, int> tuple3 = new Tuple<int, int, int>(5, 5, 5);
    var tuple4 = new Tuple<int, int, int, int>(5, 5, 5, 5);

    Console.WriteLine(tuple1.Item1);
    Console.WriteLine(tuple2.Item2);
    Console.WriteLine(tuple3.Item3);
    Console.WriteLine(tuple4.Item4);

    Console.ReadLine();
}

恕在下不知道这东西有什么用,且看看知乎上的大神们这么说:如何评价元组Tuple在C#中的作用

ExpandoObject

使用ExpandoObject可以动态的给对象添加属性,如下:

代码14

static void Main(string[] args)
{
    dynamic var = new System.Dynamic.ExpandoObject();

    var.X = 1;
    var.Y = "hello";

    Console.WriteLine(var.X);
    Console.WriteLine(var.Y);

    Console.ReadLine();
}

作为静态语言的C#,这玩意儿到底有啥用,Jeffrey说“语法看起来不错”、“可以在C#和像Python这样的动态语言之间传递ExpandoObject对象”。

有参属性

使用索引的一些注意事项:

给get和set访问器声明不同的可访问性限制

如果要将get定义为public,而将set定义为protected,可写如下代码:

代码15

public int Age
{
    get{return _age;}
    protected set(_age = value;)
}

编译器要求修饰get或set的可访问性限制(如本例中的protected)必须必属性本身的可访问性限制(如本例中的public)更强

第十一章 事件

自定义事件

以下代码是Jeffrey在书中举的也给非常规范的例子(字段的命名习惯我和Jeffery不同,他喜欢“m_”打头,我喜欢“_”打头),为了方便展示,我把它写到为了一块了:

代码16

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace EventTest
{
    class Program
    {
        static void Main(string[] args)
        {
            MailManger manager = new MailManger();
            manager.NewMail += (s,e)=> Console.WriteLine(String.Format("new mail {0} from {1} to {2}",e.From, e.To, e.Subject));
            manager.SimulateNewMail("Joey", "Jeffrey", "hello");

            Console.ReadLine();
        }
    }

    class NewMailEventArgs:EventArgs
    {
        private string _from;
        private string _to;
        private string _subject;

        public NewMailEventArgs(string from, string to, string subject)
        {
            _from = from;
            _to = to;
            _subject = subject;
        }

        public string To { get { return _to; } }
        public string From { get { return _from; } }
        public string Subject { get { return _subject; } }
    }

    class MailManger
    {
        public event EventHandler<NewMailEventArgs> NewMail;

        protected virtual void OnNewMail(NewMailEventArgs eventArgs)
        {
            EventHandler<NewMailEventArgs> temp = Volatile.Read(ref this.NewMail);
            if (temp != null)
            {
                temp(this, eventArgs);
            }
        }

        public void SimulateNewMail(string from, string to, string subject)
        {
            NewMailEventArgs eventArgs = new NewMailEventArgs(from, to, subject);
            this.OnNewMail(eventArgs);
        }
    }
}

其中,OnNewMail方法中使用了Volatile.Read(后面介绍),这样做是为了防止在多线程环境下,执行委托之前被其它线程将委托设为null。

编译器如何实现事件

对于上例中的表达式 public event EventHandler<NewMailEventArgs> NewMail;,C#编译器把它转换为了如下:

一个委托:

private EventHandler<NewMailEventArgs> NewMail = null;

一个用来登记对事件关注的方法:

public void add_NewMail(EventHandler<NewMailEventArgs> value)
{
    //将value加入到委托链
}

一个用来注销对事件关注的方法:

public void remove_NewMail(EventHandler<NewEmailEventArgs> value)
{
    //将value加入从委托链中移除
}

第十二章 泛型

泛型是一种对“算法”进行重用的机制,举例说明:一个程序员写了一个排序算法,但是他不指定要排序元素的数据类型,另一个程序员调用他写的算法时制定元素的类型即可。泛型使得同一个算法能被不同的类型复用。

使用泛型的好处:

泛型类的继承

Jeffrey举了一个例子,演示了一种巧用泛型继承的技巧,本例试图创建一个泛型链表:

代码17

class Note<T>
{
    private T _data;
    private Note<T> _next;

    public Note(T data, Note<T> next)
    {
        _data = data;
        _next = next;
    }

    public Note(T data):this(data, null)
    {

    }

    public override string ToString()
    {
        return _data.ToString() + (_next != null ? _next.ToString() : String.Empty);
    }
}


static void Main(string[] args)
{
    Note<string> headNote = new Note<string>("c");
    headNote = new Note<string>("b", headNote);
    headNote = new Note<string>("a", headNote);

    Console.WriteLine(headNote.ToString());
    Console.ReadLine();
}

上面的例子中,所构建的链表中每个元素的类型必须相同,因为方法Note(T data, Note<T> next)要求data和next的数据类型必须相同,由什么办法可以让链表的每个元素具有不同的类型呢,把T指定为Object当然是可以的,但是会出现装箱行为,性能上不是很好,Jeffrey给我们演示了用继承来解决这问题:

代码18

class NewNote
{
    protected NewNote _next;

    public NewNote(NewNote next)
    {
        _next = next;
    }
}

class NewNote<T> : NewNote
{
    private T _data;

    public NewNote(T data, NewNote next):base(next)
    {
        _data = data;
    }

    public NewNote(T data)
        : this(data, null)
    {

    }

    public override string ToString()
    {
        return _data.ToString() + (_next != null ? _next.ToString() : String.Empty);
    }
}

static void Main(string[] args)
{
    NewNote headNewNote = new NewNote<string>("c");
    headNewNote = new NewNote<int>(1, headNewNote);
    headNewNote = new NewNote<string>("a", headNewNote);

    Console.WriteLine(headNewNote.ToString());
    Console.ReadLine();
}

委托和接口的逆变和协变泛型类型实参

泛型类型的参数形式:

请看如下示例:

代码19

delegate TResult MyFun<TValue, TResult>(TValue value);
delegate TResult MyFun2<in TValue, out TResult>(TValue value);

class Test
{
    public Test()
    {
        MyFun2<Stream, IOException> fuckHim = null;
        MyFun2<FileStream, Exception> fuckHimAgain = fuckHim; //编译通过

        MyFun<Stream, IOException> fuckMe = null;
        MyFun<FileStream, Exception> fuckMeAgain = fuckMe; //编译通不过
    }
}

下面是一个泛型接口的例子:

代码20

class Program
{
    static void Main(string[] args)
    {
        FuckAble<Exception> fuck = null;
        Function(fuck);
    }

    static void Function(FuckAble<IOException> parameter)
    {

    }
}

interface FuckAble<in T>
{
    void Fuck(T t);
}

可验证性和约束

子类和实现类不能修改父类和接口中定义的虚方法的泛型类型约束,如以下代码编译失败:

代码21

class ParentClass
{
    protected virtual void Display<T>(T t) where T:Exception
    {

    }
}

class ChildClass: ParentClass
{
    protected override void Display<T>(T t) where T : Exception // 编译错误
    {
        base.Display<T>(t);
    }
}

主要约束

类型参数可以指定零个或多个主要约束。主要约束可以是一个非密类引用类型(但是不能指定以下特殊类型:Object, Array, Delegate, MulticastDelegate, ValueType, EnumVoid),指定了引用类型约束之后,相对于像编译器承诺泛型类型实参只能是约束指定类型或者其派生类。

此外,还有两个特殊约束:classstruct,前者表示泛型类型实参是引用类型,包括:类、接口、委托或数组;后者表示泛型类型实参是值类型,包括枚举和结构体。

次要约束

类型参数约束

很少用到,用来约束多个类型参数之间的关系,请看如下示例:

代码22

class Program
{
    private static void Main(string[] args)
    {
        //
        // 编译成功的
        //
        F(new IOException(), new Exception());
        F(new Exception(), new Exception());
        F(1, 1);

        //
        // 编译失败的
        //
        F("hello", 1);
        F(new IOException(), new OutOfMemoryException());
    }

    private static TBase F<T, TBase>(T value, TBase TResult) where T : TBase
    {
        return value;
    }
}

class ParentClass
{
}

class ChildClass: ParentClass
{
}

上例中,约束T必须是TBase类型或者是其派生类。

构造器约束

形如calss TestClass<T> where T:new()表示泛型类型实参是一个具有公共无参构造器的非抽象类型,包括(值类型值类型均包含公共无参构造器)

接口约束

可以约束泛型类型实参必须实现0到个接口

泛型坑1

没有类型约束的泛型参数实参可以和null进行比较,如果是值类型,t == null永远为false,t != null永远为true。但是如果约束了泛型参数类型是struct,那么与null比较会导致编译错误。

第十三章 接口

定义接口

接口中可以定义实例方法,实例属性和索引,本质上讲,属性和索引也是方法。接口还可以定义事件。接口不可以定义实例构造器,实例字段,这很容易理解,接口不需要实例化,所以构造器没用,接口也没有状态和行为的实现,所以实例字段也没用。CLR允许接口定义静态构造器,静态方法,静态字段和常量,但这些不符合CLS的规定,比如C#就不允许这样做。

接口之间的继承

接口可以继承接口,实际上此处的“继承”不同于类的继承,这样理解会很好一些:接口B继承了接口A相对于接口B把接口A中定义的方法签名容纳了进来。

派生类和父类同时实现一个接口

情况如下代码:

代码23

class Program
{
    static void Main(string[] args)
    {
        Bird bird = new Sparrow();

        bird.Fly(); // bird can fly
        ((IFlyable)bird).Fly(); // sparrow can fly

        Console.ReadLine();
    }
}

interface IFlyable
{
    void Fly();
}

class Bird : IFlyable
{
    public virtual void Fly()
    {
        Console.WriteLine("bird can fly");
    }
}

class Sparrow:Bird, IFlyable
{
    public new void Fly()
    {
        Console.WriteLine("sparrow can fly");
    }
}

上例中,bird.Fly()执行的是父类的方法,而把bird强制转换为IFlyable后,执行的派生类的方法,需要注意。当然如果给父类的Fly方法加上virtual关键字,给派生类Fly方法加上override关键字,两次调用都会是派生类的方法。

用显式接口方法实现来增强编译时类型安全和装箱问题

Jeffrey这个例子像是一个奇淫巧技,在没有泛型的日子里应该有用,且看下面的例子,注意SomeValueType和SomeValueType2的区别:

代码24

class Program
{
    static void Main(string[] args)
    {
        SomeValueType value = new SomeValueType(0);
        value.CompareTo(value); // 装箱
        value.CompareTo("hello"); // 运行时错误

        SomeValueType2 value2 = new SomeValueType2(0);
        value2.CompareTo(value2); //无需装箱
        value2.CompareTo("hello"); //编译时错误
    }
}

interface IComparable
{
    int CompareTo(Object other);
}

struct SomeValueType : IComparable
{
    private int _x;
    public SomeValueType(int x)
    {
        _x = x;
    }

    public int CompareTo(object other)
    {
        return _x - ((SomeValueType)other)._x;
    }
}

struct SomeValueType2 : IComparable
{
    private int _x;
    public SomeValueType2(int x)
    {
        _x = x;
    }

    int IComparable.CompareTo(object other)
    {
        return this.CompareTo((SomeValueType2)other);
    }

    public int CompareTo(SomeValueType2 other)
    {
        return _x - other._x;
    }
}

如果使用泛型接口可以这样:

代码25

interface IComparable<T>
{
    int CompareTo(T other);
}

struct SomeValueType : IComparable<SomeValueType>
{
    private int _x;
    public SomeValueType(int x)
    {
        _x = x;
    }

    public int CompareTo(SomeValueType other)
    {
        return _x - other._x;
    }
}

设计:基类还是接口

第十四章 字符、字符串和文本处理

字符

在.Net Framework中,字符被表示成了16位的Unicode代码值,每个字符都是System.Char(值类型)的实例

char的主要成员:

为了简化开发,char由提供了很多IsXXX静态方法,其内部调用了GetUnicodeCategory方法,如常用的由:

上面这些方法都有两个重载,一个参数为char,还有一个参数为String和表示字符在字符串中位置的int

除了上面罗列的一部分常用静态方法,char类型还提供了如下常用的实例方法:

第十五章 枚举类型和标志位

枚举类型

枚举本质上是这样的,它继承于System.Enum,并且将枚举项都定义为常量,还有一个叫做“Value__”的公共字段,且看如下代码:

代码26

enum Color:byte
{
    Red,
    Black,
    White
}

它对应的IL是这样的:

代码27

.class private auto ansi sealed Color
extends [mscorlib]System.Enum
{
.field public static literal valuetype EnumTest.Color Black = uint8(1)

        .field public static literal valuetype EnumTest.Color Red = uint8(0)

        .field public specialname rtspecialname uint8 value__

        .field public static literal valuetype EnumTest.Color White = uint8(2)

    }

位标志

将枚举值设为2的整数次幂,也就是诸如001,010,100等数字后,每个枚举值实际上标识一个标志位,可以通过按位或和按位与操作给编程带来快捷,如下示例:

代码28

class Program
{
    static void Main(string[] args)
    {
        File fileA = new File("fileA", Operate.Read);
        File fileB = new File("fileB", Operate.Read | Operate.Write);
        File fileC = new File("fileC", Operate.Read | Operate.Write | Operate.Execute);

        Console.WriteLine(fileA.Operate);
        Console.WriteLine(fileB.Operate);
        Console.WriteLine(fileC.Operate);
        Console.WriteLine(System.Environment.NewLine);

        Console.WriteLine("fileA can execute :" + ((fileA.Operate & Operate.Execute) != 0).ToString());
        Console.WriteLine("fileB can execute :" + ((fileB.Operate & Operate.Execute) != 0).ToString());
        Console.WriteLine("fileC can execute :" + ((fileC.Operate & Operate.Execute) != 0).ToString());

        Console.ReadLine();
    }
}

[Flags]
enum Operate
{
    Read = 1, // 001
    Write = 2, // 010
    Execute = 4 // 100
}

class File
{
    public File(String name, Operate operate)
    {
        this.Name = name;
        this.Operate = operate;
    }

    public String Name { get; set; }

    public Operate Operate { get; set; }
}

结果如下:
image.png-18.1kB

此处有个疑问,我去掉Flags特性,程序也能运行,只是枚举的ToString()方法得到的内容不一致。这个特性可能仅仅起到一个在ToString()方法中格式化输出的作用。

第十六章 数组

CLR支持一维数组,多维数组,和交错数组。

第十七章 委托

委托的协变和逆变

方法的返回值类型可以是委托定义的返回值的派生类,方法的参数类型可以是委托指定的参数类型的基类。比如定义如下委托:

delegate Object FeedBack2(FileStream fs);

可以用它来绑定如下方法:

static String Fuck(Stream s)
{
    return "";
}

委托揭秘

定义委托

如果定义一个委托如下:

delegate void FeedBack(int value);

编译器会将整个委托转换为一个类,名叫FeedBack。这个类继承自System.MulticastDelegate,而System.MulticastDelegate又继承自System.Delegate,System.Delegate继承自System.Object

FeedBack类定义如下:

图片.png-5.2kB

委托本质上是一个类,所以能够定义类的地方都能定义委托。

其中,构造函数的第一个参数用来传递对象(如果是静态方法,此处传递的是null),第二参数用来传递方法。调用委托的构造函数的时候,开发人员实际传递的值是类名.方法名对象实例名.方法名方法名。编译器会把它转换为两个参数,一个是Object类型的表示类或实例,另一个是IntPtr型的用来表示方法(从程序集元数据中获取方法对应的IntPtr型的值)。

每个委托型的变量实际上都指向了一个MulticastDelegate的一个派生类对象实例的地址,如以下两个委托:

FeedBack fb1 = new FeedBack(new Program().FeedBack2File);
FeedBack fb2 = new FeedBack(Program.FeedBack2Console);    

该对象是一个目标方法包装器,里面包含如下内容:

图片.png-16.7kB

调用委托

如果我们要调用fb1,如下所示:

if (fb1 != null)
{
    fb1(100);
}

这里看似调用了fb1方法,实际上并不存在fb1方法,编译器知道fb1是个委托,它会将如上调用编译为fb1.Invoke(100)。Invoice方法中会根据私有变量_target 和_methodPrt找到被包装的方法并调用它。

委托链

情况如下代码:

FeedBack fb1 = new FeedBack(new Program().FeedBack2File);
FeedBack fb2 = new FeedBack(Program.FeedBack2Console);  

FeedBac fbChain = null;
fbChain = (FeedBack)Delegate.Combine(fbChain, fb1);
fbChain = (FeedBack)Delegate.Combine(fbChain, fb2);

上述代码执行后,fbChain指向一个FeedBack对象实例,该实例的_invocationList集合中包含了fb1和fb2,如下图:

image_1ba67it751fm61cte14dfi3soar9.png-51.7kB

为了方便开发者,委托对象重载了+=-=操作符,前者相当于Delegate.Combine方法,后者相当于Deleate.Remove方法。

不难想象,Invoke方法中遍历字段InvocationList集合中的对象,被别执行其中被包装的方法。这样的设计存在两个问题:一,如果方法具有返回值,顺序执行委托链中所有方法后,只能保留最好一个方法的返回值,其余的被丢弃;二,一旦其中一个方法发生异常,后续的方法不会再被执行。

为了解决以上两个问题,MulticastDeleage类中提供了一个实例方法GetInvocationList,该方法返回委托连集合,调用者可以使用自己的算法来分别调用其中的每个方法。

使用FCL中定义的委托类型

为了方便开发者,FCL中定义了17个名为Action和17个名为Func的委托类型,如下:

image_1ba69do73pnn19lb15ma14j11e89m.png-6.3kB

image_1ba69f0vc4ts1pi98kgdpq12hk13.png-6.1kB

第十八章 特性

自定义特性

自定义特性应该尽量保持简单,不应该含有公共方法,仅用公共属性表示状态。请看如下示例:

[MyFlags("Joey", Age = 1)]
class Program
{
    static void Main(string[] args)
    {
    }
}

[AttributeUsage(AttributeTargets.Class, Inherited = true)]
class MyFlagsAttribute : Attribute
{
    String _name;

    public MyFlagsAttribute(String name)
    {
        _name = name;
    }

    public int Age { get; set; }
}

上面代码中,定义了特性MyFlags,它继承了Attribute类,并且加了特性AttributeUsage,特性本身也是一个类,所有可以给它本身加特性,AttributeTargets是一个枚举,内容如下:

image.png-5.2kB

这个枚举标识特性的应用对象类型,如果不加Usage特性,默认是可应用在所有类型

定义好特性即可使用,圆括号内前面不需要=的参数表示构造器的参数,后面需要=的参数表示属性赋值,等号前面的是属性名称,等号后面的是属性值。如下:

[MyFlags("Joey", Age = 1)]

"Joey"是构造器的参数,Age = 1表示把1赋值给属性Age。上面的特性AttributeUsage有个比较重要的属性Inherited,如将该属性设置为true,表示应用自定义特性的类或成员可以由派生类或重写成员继承。

特性的构造器、属性和字段可以使用的数据类型

只能使用如下类型:Boolean、Char、Byte、SByte、Int16、Uint16、Int32、UInt32、Int64、UInt64、Single、Double、String、Type、Object活枚举类型。此外,还有上述类型的一维0基数组,但是要尽量避免使用数组,不符合CLS规范。

检测自定义特性

给类型、成员等加上特性并不能影响其行为,加上特性后还需要在某个必要的地方读取一个类型、成员等是否应用了特性,然后再根据是否应用了特性或特性的参数创建不同的行为。以下是检测特性的一些方法:

IsDefine方法

该方法可以判断类型是否应用了特性,请看实例代码:

class Program
{
    static void Main(string[] args)
    {
        MyClass myClassInstance = new MyClass();
        bool isAttributeDefined = myClassInstance.GetType().IsDefined(typeof(MyTestAttribute), false);
        Console.WriteLine(isAttributeDefined);

        if (isAttributeDefined)
        {
            // do somthing
        }
        else
        {
            // do somthing else 
        }

        Console.ReadLine();
    }
}

class MyTestAttribute:Attribute
{
    String _name;
    public MyTestAttribute(String name)
    {
        _name = name;
    }

    public int Age { get; set; }
}

[MyTest("Joey")]
class MyClass
{

}

GetCustomAttribute和GetCustomAttributes方法

上面说的IsDefine方法仅能判断一个类型是否应用了特性,不能得到一个类型应用的特性的实例,如果需要知道特性实例,需要使用System.Reflection.CustomAttributeExtensions.GetCustomAttributeSystem.Reflection.CustomAttributeExtensions.GetCustomAttributes这两个扩展方法,请看如下代码:

class Program
{
    static void Main(string[] args)
    {
        MyClass myClassInstance = new MyClass();

        Type t = myClassInstance.GetType();
        var attreibutes = t.GetCustomAttributes();
        foreach (var item in attreibutes)
        {
            MyTestAttribute myAttribute = item as MyTestAttribute;
            if (myAttribute != null)
            {
                Console.WriteLine("Name:" + myAttribute.Name); //Name:Joey
                Console.WriteLine("Age:" + myAttribute.Age); //Age:0
            }
        }

        Console.ReadLine();
    }
}

class MyTestAttribute:Attribute
{
    public MyTestAttribute(String name)
    {
        this.Name = name;
    }

    public String Name { get; set; }

    public int Age { get; set; }
}

[MyTest("Joey")]
class MyClass
{
    public string Name { get; set; }
}

第二十章 异常和状态管理

Jeffrey说:一个方法不能完成它名称所指的功能时,就应该抛出异常。

throw和throw e

执行throw e后,异常栈重置,认为重新抛出异常的地方(调用throw e语句的地方)是异常栈的起点,执行throw并不会重置异常栈。

Jeffrey建议的设计规范

善用finally

利用finally块无论如何都回执行到的特点,可以将释放资源的代码放到finally块中。实际上,只要使用lockforeachusing编译器就会生成try/finally块。

不要什么都捕捉

应该只捕捉可能抛出的异常,所有的异常都捕捉,并且不再throw的话,会造成错误不容易被发现。(个人认为,不要在底层代码吞噬所有未知异常,顶层代码可以适当试图捕获所有异常)

得体地从异常中恢复

如:

try
{
    //do somthing
}
catch (IOException ex)
{
    IILogBll.Addlog("CompanyBasicInfoImporter.Import", ex);
    errorMessage = "无法读取" + fileShortName + "文件或文件格式有误";
}
catch(ZipException ex)
{
    IILogBll.Addlog("CompanyBasicInfoImporter.Import", ex);
    errorMessage = fileShortName +"文件格式有误";
}
catch (POIXMLException ex)
{
    IILogBll.Addlog("CompanyBasicInfoImporter.Import", ex);
    errorMessage = fileShortName +"文件格式有误";
}

发生不可恢复的异常时回滚部分已完成操作

如:

try
{
    // 记录操作前的状态
    // 开始操作
}
catch (Exception ex)
{
    // 回滚操作

    // 重新抛出,让上层代码知道发生了什么
    throw;
}

隐藏实现细节

Jeffrey给出一个例子:

public String GetPhoneNumber(String name)
{
    String phone;
    FileStream fs = null;
    try
    {
        fs = new FileStream(_filePath, FileMode.Open);
        phone = /* 已找到的电话号码 */
    }
    catch (FileNotFoundException e)
    {
        throw new NameNotFoundException(name, e);
    }
    catch(IOException e)
    {
        throw new NameNotFoundException(name, e);
    }
    finally
    {
        if (fs != null)
        {
            fs.Close();
        }
    }
    return phone;
}

NameNotFoundException:

[Serializable]
internal class NameNotFoundException : Exception
{
    public NameNotFoundException(string message, Exception innerException) : base(message, innerException)
    {
    }
}

该例中,调用方法的用户不需要知道方法实现的细节,他们不需要知道通讯录是保存在文件中还是数据库等载体,只要知道NameNotFoundException这个异常就好了,将原来的异常作为新异常的内部异常,这样便于除错。

有时候,开发人员捕获了一个异常,仅仅是希望再添加一些说明信息后再抛出,这时候可以在Data属性中添加数据如:

try
{
    // 开始操作
}
catch (Exception ex)
{
    ex.Data.Add("fileName", fileName);
    throw;
}

未处理的异常

程序抛出了异常,但是没有匹配的Catch块来处理,CLR检测到进程中有任何线程有未处理的异常时,停止进程,windows会记录事件日志,这是个bug。

当然,也可以开发程序让CLR来处理为不处理异常,但是微软没有统一这个模型,不同种类的应用程序使用不同的代码,如:

CER

代码协定

第二十一章 托管代码和垃圾回收

从托管堆分配资源

进程初始化时,CLR划分出一片地址空间作为托管堆,并维护一个用来指向下一个对象地址的指针NextObjPtr,刚开始,它指向托管堆的基地址。

一个区域被非垃圾对象填满后,CLR会划分新的地址空间扩充托管堆,如此重复,知道进程地址空间被用光。32位系统进程最多可分配1.5G地址空间,64位系统进程最多可分配8T地址空间。

new一个对象时,CLR执行了如下操作:

垃圾回收算法

  1. 当程序调用new操作时,如果没有足够的空间创建对象就会执行一次垃圾回收
  2. CLR暂停所有的线程,以免在标记对象的时候对象的状态被改变
  3. CLR将所有的目标对象标记为“垃圾”(将同步索引块的某位标记为0)
  4. 引用变量被称为“跟”,CLR检查所有的跟,如果跟不为null,那么它指向的对象就是“可达的”,把可达对象标记为“非垃圾”(将同步索引块中的某一位标记为1)
  5. 可达的对象也会包含跟(引用变量),当CLR标记可达对象时,也会继续判断可达对象里的跟是否是null,如果不是,标记这些跟指向的对象为“非垃圾”,如此递归,直到所有的跟都被检查了一遍。再标记对象的过程中,可能会遇到已经标记为“非垃圾”的对象,遇到这样的对象则跳过。
  6. 压缩空间,将地址不连续的非垃圾对象移位,使所有的对象连续,同时修改指向这些对象的变量的值,使它们的值等于对象移动后的地址
  7. 更新NextObjPtr的值,指向最后一个对象的末尾
  8. 垃圾回收会如果买没有足够的空间创建对象,那么CLR会划分新的地址空间扩充托管堆
  9. 如果进程的地址已经全部用完,无法再分配新的地址来扩充托管堆,那么抛出OutOfMemoryException异常

以上仅是算法描述,没有考虑对象的“代”。

注意 静态字段指向的对象会一直存在,除非卸载AppDomain,如果静态变量指向的集合对象体积不断变大很可能会引起内存溢出,开发的时候需要注意

GC全程generational garbage collector,是一种基于“代”的垃圾回收。它对代码做出了以下几点假设:

实际上垃圾回收不是每次都扫描所有的对象,而是先扫描新对象,后扫描老对象,具体如下:

  1. 托管堆上第一批对象为第0代对象
  2. 当new对象时,先检查给0代对象预置的空间里是否有足够空间创建新对象
  3. 如果第0代对象暂满了预置的空间,则对第0代对象进行垃圾回收
  4. 进行过一次垃圾回收后剩余的0代对象升为1代对象
  5. 新的对象成了新的0代对象
  6. 当1代对象占满了预置的空间,则对1代对象和0代对象进行回收,剩余的1代对象升为2代对象

CLR的垃圾回收机制中共有3代对象。

大对象的情况有些不同:

第二十三章 程序集的加载和反射

程序集加载

更多方法请参见System.Reflection.Assembly类型

发现程序集中定义的类型

static void Main(string[] args)
{
    Assembly assembly = Assembly.LoadFile(@"C:\Users\Administrator\Documents\Visual Studio 2015\Projects\ReTest\Lib\bin\Debug\Lib.dll");
    var types = assembly.ExportedTypes;
    foreach (var item in types)
    {
        Console.WriteLine(item.ToString());
    }

    Console.ReadLine();
}

构造类型的实例

得到了类型的对象类型Type,就可以实例化对象了:

下面是一个构造泛型实例的例子:

static void Main(string[] args)
{
    Assembly assembly = Assembly.LoadFile(@"C:\Users\Administrator\Documents\Visual Studio 2015\Projects\ReTest\Lib\bin\Debug\Lib.dll");
    var types = assembly.ExportedTypes;
    foreach (var item in types)
    {
        Type openType = typeof(List<>);
        Type closedType = openType.MakeGenericType(item);
        Object obj = System.Activator.CreateInstance(closedType);

        Console.WriteLine(obj.ToString()); // System.Collections.Generic.List`1[Lib.Class1]
    }

    Console.ReadLine();
}

使用反射发现类型的成员

字段,方法,属性,事件,内部类都算类型的成员。抽象类System.Reflection.MemberInfo封装了这些成员共有的属性,其派生类分别对应上述成员,如下图:

image_1bargrshtl781sh1fnjblmp213.png-64.4kB

如下为实例代码:

static void Main(string[] args)
{
    Assembly assembly = Assembly.LoadFile(@"C:\Users\Administrator\Documents\Visual Studio 2015\Projects\ReTest\Lib\bin\Debug\Lib.dll");
    var types = assembly.ExportedTypes;
    foreach (var item in types)
    {
        var members = item.GetMembers();
        foreach (var member in members)
        {
            Console.WriteLine(member.GetType().ToString() + " " + member.ToString());
        }
    }

    Console.ReadLine();
}

运行结果:

image_1bargcqfmtegkdj5qu16b917jom.png-37kB

调用类型的成员

成员类型 调用成员而需要调用的方法
FieldInfo GetValue、SetValue
ConstrucorInfo Invoke
MehodInfo Invoke
PropertyInfo GetValue、SetValue
EventInfo AddEventHandler、RemoveEventHandler

此外,每个代表成员类型的类型都有一些独有的属性和方法,详情请按F12。

第二十四章 运行时序列化

快速入门

class QuickStart
{
    public static MemoryStream Serialize2Memmory(Object obj)
    {
        MemoryStream stream = new MemoryStream();
        BinaryFormatter formatter = new BinaryFormatter();
        formatter.Serialize(stream, obj);

        return stream;
    }

    public static Object DeserializeFromMemmory(Stream stream)
    {
        BinaryFormatter formatter = new BinaryFormatter();
        return formatter.Deserialize(stream);
    }
}

static void Main(string[] args)
{
    List<string> objectGraph = new List<string> { "Jeffrey", "Joey" };
    Stream stream = QuickStart.Serialize2Memmory(objectGraph);

    stream.Position = 0;
    Object obj = QuickStart.DeserializeFromMemmory(stream);
}

可以利用序列化实现对象的深拷贝:

public static T DeepClone<T>(T t) where T : new()
{
    using (Stream stream = new MemoryStream())
    {
        BinaryFormatter formatter = new BinaryFormatter();
        formatter.Context = new StreamingContext(StreamingContextStates.Clone); // 请看下文
        formatter.Serialize(stream, t);

        stream.Position = 0;
        return (T)formatter.Deserialize(stream);
    }
}

可以将多个对象图序列化到同一个流中。如下代码:

static void Main(string[] args)
{
    List<string> objectGraph1 = new List<string> { "Jeffrey", "Joey" };
    List<string> objectGraph2 = new List<string> { "Jeffrey2", "Joey2" };
    List<string> objectGraph3 = new List<string> { "Jeffrey3", "Joey3" };

    BinaryFormatter formatter = new BinaryFormatter();

    MemoryStream stream = new MemoryStream();
    formatter.Serialize(stream, objectGraph1);
    formatter.Serialize(stream, objectGraph2);
    formatter.Serialize(stream, objectGraph3);

    stream.Position = 0;
    List<string> newObjectGraph1 = (List<String>)formatter.Deserialize(stream);
    List<string> newObjectGraph2 = (List<String>)formatter.Deserialize(stream);
    List<string> newObjectGraph3 = (List<String>)formatter.Deserialize(stream);

    DisPlayList(newObjectGraph1);
    DisPlayList(newObjectGraph2);
    DisPlayList(newObjectGraph3);

    Console.ReadLine();
}

private static void DisPlayList(List<string> obj)
{
    foreach (var item in obj)
    {
        Console.Write(item + " ");
    }
    Console.WriteLine();
}

运行结果:

image_1barmsp1t1u0212mi1291hr2qd41g.png-4.1kB

使类型可序列化

如果对象图中有不可序列化的对象,那么调用Serialize方法是会抛出SerializationException异常。

注意 在抛出SerializationException异常之前,可能已经有一部分对象序列化到了流中,流中会包含已经损坏的对象,需要得体地恢复。一个方法是:现将对象实例化到临时的Stream中,当序列化完成后再将临时流合并到目标流中。

应用特性SerializableAttribute的类、委托、结构体和枚举可以被序列化。枚举和委托总是可序列化的,可以不用加该特性。该特性不会被派生类继承。

除了SerializableAttribute特性,还有几个常用特性:

上面列出的5个特性可以被派生类继承,应用这这些特性的方法参数必须是StreamingContext类型。下面是个应用NonSerialized和OnDeserialized的例子:

[Serializable]
public class Cricle
{
    private double _radius;

    [NonSerialized]
    private double _area;

    public Cricle(double radius)
    {
        _radius = radius;
        _area = Math.PI * _radius * _radius;
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        _area = Math.PI * _radius * _radius;
    }
}

实现ISerializable接口来使类型可序列化

第二十六章 线程基础

线程开销

CLR线程和Windows线程

CLR线程完全等价于windows线程,微软曾经试图开发非映射到操作系统的线程,但是最终放弃。

使用专门的线程来执行异步的计算密集型操作

推荐使用线程池来执行计算密集型操作,但是如果满足一下条件任意一条的话可以考虑创建专门的线程:

以下代码演示如何创建一个线程并异步调用一个操作:

class Program
{
    static void Main(string[] args)
    {
        Thread thread = new Thread(DoComputerBoundOperation);
        thread.Start("Joey");
        Console.WriteLine("main thread...");
        Thread.Sleep(2 * 1000);// 模拟做其它工作

        thread.Join(); //阻塞当前线程直到thread代表的线程终止

        Console.WriteLine("enter any key to exit");
        Console.ReadLine();
    }

    static void DoComputerBoundOperation(Object state)
    {
        Console.WriteLine("begin a compute-bound operation, sate is " + state.ToString() );
        Thread.Sleep(3 * 1000);
        Console.WriteLine("end the compute-bound operation");
    }
}

使用线程的理由

线程调度的优先级

抢占式操作系统必须判断什么时候调度哪个线程多长时间。windows为线程分配了从0到30的优先级,高优先级的线程总是抢占低优先级的线程。为了方便开发人员,Windows开放了一个线程优先级抽象层:线程的优先级可以分为:
- Idle
- Lowest
- Below Normal
- Normal(默认)
- above Normal
- Highest
- Time-Critical

实际上进程也有一个优先级抽象,和线程的优先级分类相同。进程的优先级没什么实际作用,只是操作系统在判断一个线程的优先级时会用线程所在进程优先级结合线程自己的优先级算出一个1到30的值来。

前台线程和后台线程

CLR将所有线程要不视为前台线程,要不视为后台线程,当所有的前台线程停止运行后,后台线程被强制停止,且不会抛出异常。

new一个Thread对象创建的线程默认是前台线程,线程池中的线程默认是后台线程。只有是那些确保要完成的工作才放到前台线程,Jeffrey建议尽量使用后台线程。以下代码展现了前台线程和后台线程的区别:

class Program
{
    static void Main(string[] args)
    {
        Thread thread = new Thread(DoComputerBoundOperation);
        thread.IsBackground = true; // 不加这句进程瞬间结束,加上这句进程3秒后结束
        thread.Start("Joey");
    }

    static void DoComputerBoundOperation(Object state)
    {
        Console.WriteLine("begin a compute-bound operation, sate is " + state.ToString() );
        Thread.Sleep(3 * 1000);
        Console.WriteLine("end compute-bound operation");
    }
}

第二十七章 计算密集型的异步操作

CLR线程池基础

线程的创建、销毁和切换都是耗费资源的行为。CLR包含了自己的线程池,且只包含一个,所有的AppDomain共享之。

线程池维护了一个操作请求队列,CLR初始化时,队列内没有记录项,也没有线程。当发起一个异步操作请求后,线程池将一个记录项保存到队列中,并创建一个线程来执行它,执行完毕后,线程不会被消化,而是进入空闲状态 。等待响应另一个请求。由于线程不销毁,减少了线程的创建和销毁所消耗的资源。

如果应用程序发齐了多个操作请求,线程池会试图使用已经存在的线程来处理,如果请求发起的速度大于线程能够处理的速度,线程池会创建新的线程。

线程池中的线程如果长时间没有处理操作请求,那么它自己醒来并销毁自己,以节省内存。

使用线程池执行简单的计算密集型操作

使用ThreadPool类的这两个方法可以让线程池执行callBack委托指定的方法:

public static bool QueueUserWorkItem(WaitCallback callBack);
public static bool QueueUserWorkItem(WaitCallback callBack, object state);

执行上下文

每个线程都关联一个执行上下文数据结构,执行上下文中包含:

当一个线程调用另一个辅助线程时,执行上下文会流入辅助线程。这确保了辅助线程的安全设置和宿主设置和调用它的线程是一致的。执行上下文在线程之间流动会照成性能的损失。System.Threading命名空间提供了一个类ExcutionContext。它允许开发者控制执行上下文是否在线程间流动:

class Program
{
    static void Main(string[] args)
    {
        CallContext.LogicalSetData("name", "Joey");

        ThreadPool.QueueUserWorkItem(DoComputerBoundOperation);

        ExecutionContext.SuppressFlow();
        ThreadPool.QueueUserWorkItem(DoComputerBoundOperation);

        Console.ReadLine();
    }

    static void DoComputerBoundOperation(Object state)
    {
        Console.WriteLine("name:" + CallContext.GetData("name"));
    }
}

图片.png-1.6kB

协作式取消和超时

.Net Framework提供了取消操作模式,“协作式”的意思是操作本身支持取消,可以通过类System.Threading.CancellationToken实现之:

class Program
{
    static void Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        ThreadPool.QueueUserWorkItem(s=>Count(cts.Token, 10));
        Console.WriteLine("press Enter to cancel");
        Console.ReadLine();
        cts.Cancel();
        Console.ReadLine();
    }

    private static void Count(System.Threading.CancellationToken token, int count)
    {
        for (int i = 0; i < count; i++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("cancel");
                break;
            }
            Thread.Sleep(1000);
            if (i == count - 1)
            {
                Console.WriteLine("done");
            }
        }
    }
}

如果执行的是不可取消的操作,可以传递CancellationTokenSource的数学None,它是一个特殊的CancellationToken对象,不用任何CancellationTokentSource关联,其IsCancellationRequested属性永远为false,其CanBeCanceled属性永远为false。

还可以为取消动作注册回调方法,如下示例:

cts.Token.Register(()=>Console.WriteLine("canceled"));

CancellationTokenSource还提供了一个“取消链”的功能,链中任意一个操作取消,链也会取消:

    static void Main(string[] args)
    {
        CancellationTokenSource cts1 = new CancellationTokenSource();
        cts1.Token.Register(()=>Console.WriteLine("cts1 cancel"));

        CancellationTokenSource cts2 = new CancellationTokenSource();
        cts2.Token.Register(() => Console.WriteLine("cts2 cancel"));

        CancellationTokenSource ctsLink = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
        ctsLink.Token.Register(()=>Console.WriteLine("ctsLink cancel"));

        cts1.Cancel();

        Console.ReadLine();
    }

输出如下:

image.png-3.2kB

最后,CancellationTokenSource还提供一个示例方法CancelAfter,该方法可以实现延迟取消,达到了超时的效果(如果一段时间内操作未完成则取消)。

任务

ThreadPool很好用,但有许多限制,最大的问题是没有内建的机制让开发者知道操作什么时候完成,也没有机制在操作完成时获得返回值。为了克服这些限制,Microsoft引入了“任务”的概念。通过System.Threading.Tasks命名空间的类来完成任务:

class Program
{
    static void Main(string[] args)
    {
        new Task(DoComputerBoundOperation, "state").Start();
        Task.Run(()=>DoComputerBoundOperation("state"));
        Console.ReadLine();
    }

    static void DoComputerBoundOperation(object state)
    {
        Console.WriteLine("state:" + state);
        Thread.Sleep(5000);
        Console.WriteLine("Done");
    }
}

实例方法`Start`或静态方法`Run`均能启动线程。

等待任务完成并获得结果

Task类的Wait方法会堵塞父线程知道任务执行完毕,调用Task类的属性Result也会造成父线程堵塞,直到得到任务的运行结果,如下:

class Program
{
    static void Main(string[] args)
    {
        Task<string> task = new Task<string>(DoComputerBoundOperation, "joey");
        task.Start();
        Console.WriteLine(task.Result);
        Console.WriteLine("main thread is running");

        Console.ReadLine();
    }

    static string DoComputerBoundOperation(object state)
    {
        Thread.Sleep(5000);
        Console.WriteLine("chilid thread done");
        return "hello task";
    }
}

运行结果:

图片.png-2.3kB

如果任务出现异常,在没有调用Wait方法或Result属性时,异常会被吞噬,如下实例代码执行后并不会出现运行错误:

class Program
{
    static void Main(string[] args)
    {
        Task<string> task = new Task<string>(DoComputerBoundOperation, "joey");
        task.Start();
        Console.WriteLine("main thread is running");

        Console.ReadLine();
    }

    static string DoComputerBoundOperation(object state)
    {
        throw new NotImplementedException();
    }
}

下面是调用属性Result(或Wait方法)时的情况:

class Program
{
    static void Main(string[] args)
    {
        Task<string> task = new Task<string>(DoComputerBoundOperation, "joey");
        task.Start();
        try
        {
            Console.WriteLine(task.Result);
        }
        catch (AggregateException ex) 
        {
            foreach (var item in ex.InnerExceptions)
            {
                Console.WriteLine(item.GetType());
            }
        }

        Console.WriteLine("main thread is running");

        Console.ReadLine();
    }

    static string DoComputerBoundOperation(object state)
    {
        throw new NotImplementedException();
    }
}

AggregateException异常中有一个属性InnerExceptions,该属性是个异常集合,集合中的元素即任务实际抛出的异常,上面的代码运行结果如下:

图片.png-2.6kB

除了实例方法Wait,Task类还提供了两个静态方法:WaitAll和WaitAny,参数为Task类型的数组。其功能望名知意。

取消任务

class Program
{
    static void Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        Task<string> task = new Task<string>(()=>DoComputerBoundOperation(cts.Token));
        task.Start();
        Console.WriteLine("press Enter to cancel");
        Console.ReadLine();
        cts.Cancel();

        try
        {
            Console.WriteLine(task.Result);
        }
        catch (AggregateException ae) 
        {
            ae.Handle(ex =>
            {
                if (ex is OperationCanceledException)
                {
                    Console.WriteLine("child thread has been Canceled");
                    return true; 
                }
                else
                {
                    Console.WriteLine("ex: "+ ex.GetType().ToString());
                    return false;
                }
            });
        }

        Console.Read();
    }

    static string DoComputerBoundOperation(CancellationToken token)
    {
        for (int i = 0; i < 5; i++)
        {
            token.ThrowIfCancellationRequested();
            Thread.Sleep(1000);
        }
        return "child thread has done";
    }
}

此处使用ThrowIfCancellationRequested()来取消操作,调用该方法会抛出一个OperationCanceledException异常,父线程中可以通过判断异常类型来获知是因为调用了cts.Cancel()而导致了子线程取消还是发生了别的什么异常。

任务完成时自动启动新任务

Task类的实例方法ContinueWith可以指定任务完成后执行的操作,如下所示:

class Program
{
    static void Main(string[] args)
    {
        Task task = new Task(() => Console.WriteLine("task 1"));
        task.ContinueWith(t=> Console.WriteLine("task 2"));
        task.ContinueWith(t=> Console.WriteLine("task 3"));
        task.ContinueWith(t=> Console.WriteLine("task 4"));

        task.Start();

        Console.ReadLine();
    }
}

执行结果如下,请足以执行顺序:

图片.png-2.3kB

子任务

请看如下代码:

class Program
{
    static void Main(string[] args)
    {
        Task parentTask = new Task(() =>
        {
            new Task(() => 
            {
                Thread.Sleep(1000);
                Console.WriteLine("child task 1");
            },TaskCreationOptions.AttachedToParent).Start();

            new Task(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("child task 2");
            }, TaskCreationOptions.AttachedToParent).Start();
        });

        parentTask.ContinueWith(t=>Console.WriteLine("parent task has done "));
        parentTask.Start();

        Console.ReadLine();
    }
}

运行结果如下:

图片.png-2.5kB

在上面的代码中,parentTask中启动了2个子任务,只有这个2个子任务完成后,parentTask才算完成。关键在于Task的构造器中的TaskCreationOptions参数,如果不指定TaskCreationOptions.AttachedToParent,就没有这种效果,去掉那个参数再执行结果是这样的:

图片.png-2.6kB

任务工厂

任务调度器

TaskScheduler类负责执行被调度的任务,FCL提供了两个派生自TaskScheduler的类型:线程池任务调度器(thread pool task scheduler)和同步上下文任务调度器(synchronization context task scheduler)。默认情况下,任务使用线程池任务调度器。同步上下文任务调度器适合于winform和WPF等图形用户界面应用程序,它将所有的任务都调度给GUI线程(也就是说会阻塞UI线程,当这个任务运行很久会使得UI卡顿,那么这玩意儿该如何用,Jeffrey的例子很棒,就是稍微复杂,我把他的例子里的“取消”功能去掉了,比较容易理解),使所有的任务代码都能成功更新UI组件。该调度器不使用线程池。可执行TaskScheduler的静态方法FromCurrentSynchronizationContext来获得同步上下文任务调度器的引用。请看示例:

下面这个程序完成这样的任务:后台执行要给费事操作,不堵塞UI线程,当后台任务执行完毕后再启动一个任务,改任务会非时操作的执行结果显示的UI组件上。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        this.StartTask();
    }

    private void StartTask()
    {
        Task<int> task = new Task<int>(()=> Sum(30));

        TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext();
        task.ContinueWith(t => this.ShowResult(t.Result), scheduler);

        task.Start();
    }

    private void ShowResult(int result)
    {
        this.labelInfo.Text = result.ToString();
    }

    private int Sum(int count)
    {
        int sum = 0;
        for (int i = 0; i < count; i++)
        {
            Thread.Sleep(100);
            sum += i;
        }
        return sum;
    }
}

Parallel的静态For,ForEach和Invoke方法

Parllel类提供了3个静态方法,内部使用Task实现,可以使用这3个方法简化执行并行任务的代码:

class Program
{
    static void Main(string[] args)
    {
        Parallel.For(0, 100, i => Console.WriteLine(i));

        List<int> list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        Parallel.ForEach(list, item => Console.WriteLine(item));

        Parallel.Invoke(() => Console.WriteLine("hello"),
            () => Console.WriteLine("world"),
            () => Console.WriteLine("live"));

        Console.Read();
    }
}

如果任务抛出异常,调用Parallel会抛出AggregateException。
此外,Parallel的三个静态方法还提供了一些重载,感兴趣的朋友可深入学习。

PLINQ

并行语言集成查询,简称PLINQ。对一个并行集合的查询时系统会把任务配给多个CPU进行,从而提高了执行性能,详见ParallelQuery和ParallelQuery类型及静态类ParallelEnumerable中的扩展方法。把一个普通集合转化为并行集合可以调用扩展方法AsParallel。如:

List<int> list = new List<int>();
for (int i = 0; i < 10000; i++)
{
    list.Add(i);
}

ParallelQuery<int> parallelQuery = list.AsParallel();

定制执行计算密集型操作

使用System.Threading.Timer

System.Threading.Timer可以让一个线程池定时调用一个方法。这里有个问题,如果要执行的回调方法费时很长,以至于第二次回调方法的时候第一次的执行还没有完成,这时候线程池会分配新的线程来执行第二次操作。如果开发人员不想程序出现这种情况,那么可以这么办:把操作执行间隔时间设置为Timeout.Infinite。在回调方法内部的最后使用Change方法设置执行时间:

class Program
{
    private static Timer _timer;
    static void Main(string[] args)
    {
        _timer = new Timer(Do, null, 500, Timeout.Infinite);
        Console.ReadLine();
    }

    private static void Do(object state)
    {
        Thread.Sleep(1500);
        Console.WriteLine("Do");
        _timer.Change(1000, Timeout.Infinite);
    }
}

FCL中的计时器

FCL不止提供了一个计时器,先分别说明如下:

线程池如何管理线程

Jeffery建议大家不要试图搞清楚线程池的工作细节,把它当成黑盒来使用就好了,因为线程池内容的细节不同版本的CLR会有不同的设计,总体来说是越来越好的。其次Jeffery也不建议自己搞一个线程池,因为很难搞出一个比CLR提供的更好的。

关于设置线程池的线程数

CLR运行开发人员设置线程池的最大线程数,但Jeffery建议不要设置最大线程数,因为可能会遇到死锁的问题,比如如果把最大线程数设置为n,恰好前n个线程正在等待第n+1个线程工作项发出解除阻塞的信号,但是第n+1线程永远也不会执行。

目前默认的最大线程池大概是1000,已经足够使用,考虑到线程的创建大约需要1M的内存,一个win32进程最大有2G可以空间,去除CLR所需空间(大约0.5G),大概最多可以创建1360个线程。64位进程理论上可以创建千百万个线程,但是创建太多的线程会造成资源的浪费,Jeffery任务如果真的需呀创建上千个线程,那么应用程序的设计可能存在问题了。

管理工作者线程

线程池将所有的待执行工作放到一个全局的队列中,多个工作者从队列中取出任务时需要一个线程同步锁,这个锁可能会在某些应用程序中照成瓶颈。使用Task不用担心这个问题,在使用默认的TaskScheduler情况下,每个工作者线程都有自己的一个放置本地工作项的队列(后进先出),不需线程锁,当本地工作项被执行完毕后,工作线程会偷别的工作者线程工作项队列首部的工作项,这个时候需要线程同步锁,但是比起线程池,同步问题大大减少了。如果所有的工作线程的本地工作项队列都为空,那么会从全局工作项队列中查找工作项,这个时候需要上线程同步锁。如果全局工作项队列也为空,那么工作者线程会休眠,超过一定时间后会唤醒自己并销毁自己。

I/O 密集型线程

C#的异步函数

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注