《CLR via C#》笔记:第2部分 设计类型(4)

  1.7 C#, 3.2 游戏引擎技术
  • 本博客所总结书籍为《CLR via C#(第4版)》清华大学出版社,2021年11月第11次印刷(如果是旧版书籍或者pdf可能会出现书页对不上的情况)
  • 你可以理解为本博客为该书的精简子集,给正在学习中的人提供一个“glance”,以及对于部分专业术语或知识点给出解释/博客链接。
  • 【本博客有如下定义“Px x”,第一个代表书中的页数,第二个代表大致内容从本页第几段开始。(如果有last+x代表倒数第几段,last代表最后一段)】
  • 电子书可以在博客首页的文档-资源归档中找到,或者点击:传送门自行查找。如有能力请支持正版。(很推荐放在竖屏上阅读本电子书,这多是一件美事)
  • 欢迎加群学习交流:637959304 进群密码:(CSGO的拆包密码) 

第十章 属性

无参属性

  • 封装对类型中的数据字段的访问,好处(P202 1)
    1、可以让访问字段来执行一些额外作用(side effect)、缓存某些值或者推迟创建一些内部对象。
    2、可以以线程安全的方式访问字段。
    3、字段可能是一个逻辑字段,它的值不由内存中的字节表示,而是通过某个算法来计算获得
  • 使用get,set封装数据(P203 1),下述的get访问器方法不接受参数
public sealed class Employee
{
    private string m_Name;
    private int m_age;

    public string Name
    {
        get{return (m_Name);}
        set{m_Name = value;}//关键字value代表新的值
    }
}

Employee e;
e.Name = "abc";
string a = e.Name;
  • 自动实现的属性(Automatically Implemented Property,AIP):public string Name{get;set;}
    使用AIP即创建了一个属性,访问该属性的任何代码实际都会调用get和set方法。
  • AIP有如下问题:(P205 2)
    1、字段声明语法可能包含初始化部分,所以要在一行代码中声明并初始化字段。但没有简单的语法初始化AIP。所以,必须在每个构造器方法中显式初始化每个AIP。
    2、运行时序列化引擎将字段名持久存储到序列化的流中。AIP的支持字段名称由编译器决定,每次重新编译代码都可能更改这个名称。因此,任何类型只要含有一个AIP,就没办法对该类型的实例进行反序列化。在任何想要序列化或反序列化的类型中,都不要使用AIP功能。
    3、调试时不能在AIP的get或set方法上添加断点,所以不好检测应用程序在什么时候获取或设置这个属性。相反,手动实现的属性可设置断点,查错更方便。
  • 对象和结合初始化器:即可以在定义时候就使用{}对其公共成员复制,并支持完成一些额外操作。(P208 2)
//如果属性的类型实现了IEnumerable或 IEnumerable<T>接口,属性就被认为是集合,而集合的初始化是一种相加(additive)操作,而非替换(replacement)操作。示例:
className c = new className{stringA = "aaa","bbb","ccc"};
  • 匿名类型:用简洁的语法来自动声明不可变的元组类型。
//方法1
var o1 = new {Name = "abc",year = 7777};
//方法2
string name = "abc";
DateTime dt = DateTime.Now;
var o2 = new{name,dt.year};
  • System.Tuple类型:从Object派生,也是匿名类型。示例:(P212 last)

有参属性

  • C#称有参数的get访问器为索引器(P214 1)
    C#使用数组风格的语法来公开有参属性(索引器)。换句话说,可将索引器看成是C#开发人员对[]操作符的重载。(P214 2)
public sealed class BitArray
{
    //容纳了二进制位的私有字节数组
    private Byte [] m_byteArray;
    private Int32 m_numBits;
    //下面的构造器用于分配字节数组,并将所有位设为0
    public BitArray ( Int32 numBits)
    {
        //先验证实参
        if(numBits <=0)
            throw new ArgumentOutOfRangeException ("numBits must be > 0");
        //保存位的个数
        m_numBits = numBits;
        //为位数组分配字节
        m_byteArray = new Byte [ (numBits + 7)/ 8 ];
    }
    //下面是索引器(有参属性)
    public Boolean this [ Int32 bitPos]
    {
        //下面是索引器的get访问器方法
        get 
        {
            //先验证实参
            if( (bitPos < 0)ll(bitPos >= m_numBits))
                throw new ArgunentOutOfRangeException ( "bitPos");
            //返回指定索引处的位的状态
            return (n_bytearray [bitPos / 8 ] 6 (1 <<(bitPos % 8) ) ) != 0;
        }
        //下面是索引器的set访问器方法
        set 
        {
            if ( (bitPos < 0)ll(bitPos >= m_numBits))
            throw new ArgumentoutOfRangeException ( "bitPos",bitPos.Tostring ( ) );
            if (value) 
            {
                //将指定索引处的位设为true
                m_byteArray [bitPos i / 8]=(Byte)(m_byteArray [bitPos / 8]l (l <<(bitPos $ 8) ) );
            }
            else 
            {
                //将指定索引处的位设为false
                m_byteArray [bitPos / 8 ]=(Byte)(m_byteArray [bitPos / 8 ] 6~(1 <<(bitPos % 8) ) );
            }
        }
    }
}

//使用:
BitArray ba = new BitArray(14);
ba[1] = 2;

调用属性刚问器方法时的性能

  • 简单的get,set,JIT编译器会将代码内联(inline)。这样就没有性能上的损失。(P218 last)

属性访问器的可访问性

  • 有时希望为get访问器方法指定一种可访问性,为set访问器方法指定另一种可访问性。最常见的情形是提供公共get 访问器和受保护set访问器。(P219 1)
//Name属性本身声明为public属性,意味着get访问器方法是公共的,所有代码都能调用。而 set访问器方法声明为protected,只能从SomeType 内部定义的代码中调用,或者从SomeType的派生类的代码中调用。
public class a 
{
    private string m_name;
    public string name
    {
        get{return m_name;}
        protected set{m_name = value;}
    }
}

第十一章 事件

  • 定义了事件成员的类型允许类型(或类型的实例)通知其他对象发生了特定的事情。定义了事件成员的类型能提供以下功能:(P221 1)类型之所以能提供事件通知功能,是因为类型维护了一个已登记方法的列表。事件发生后,类型将通知列表中所有已登记的方法。
    1、方法能等级它对事件的关注
    2、方法能注销它对事件的关注
    3、事件发生时,登记了的方法将接收到通知
  • CLR事件模型以委托为基础。委托是调用”回调方法的一种类型安全的方式。对象凭借回调方法接收它们订阅的通知。

设计要公开事件的类型

image 49 - 《CLR via C#》笔记:第2部分 设计类型(4)
  • 根据上图示例来进行设计:
//第一步:定义类型来容纳所有需要发送给事件通知接受者的附加信息(P222 last)
public class a 
{
    private string m_name;
    public string name
    {
        get{return m_name;}
        protected set{m_name = value;}
    }
}

internal class NewMailEventArgs : EventArgs
{
    private readonly string m_from,m_to,m_subject;

    public NewMailEventArgs(string from,string to,string subject)
    {
        m_from = from;m_to = to,m_subject = subject;
    }

    public string From{get{return m_from;}}
    public string To{get{return m_to;}}
    public string Subject{get{return m_subject;}}
}


//第二步:定义事件成员-event(P223 last)
//每个事件成员都要指定一下内容:可访问性标识符;委托类型,指出要调用的方法的原型;名称(可以是有效标识符)
iinternal class MailManager
{
    public event EventHandler<NewMailEventArgs> NewMail;
}


//第三步:定义负责引发事件的方法来通知事件的登记对象(P224 last)
//类需要定义一个受保护的虚方法,引发事件时,类及其派生类中的代码会调用该方法。
internal class MailManager
{
//第三步:定义负责引发事件的方法来通知已登记的对象。
//如果类是密封的,该方法要声明为私有和非虚
    protected virtual void OnNewMail(NewMailEventArgs e)
    {
//出于线程安全的考虑,现在将对委托字段的引用复制到一个临时变量中(P225 1)
        EventHandler<NewMailEventArgs > temp = Volatile.Read(ref NewMail);
//任何方法登记了对事件的关注,就通知它们
        if(temp != null) temp(this,e);
    }
}


//第四步:定义方法将输入转化为期望事件(P226 4)
internal class MailManager
{
    public void SimulateNewMail(string from,string to,string subject)
    {
        NewMailEventArgs e = new NewMailEventArgs(from,to,subject);
//调用虚方法通知对象事件已经发生
//如果没有类型重写该方法,我们的对象将通知事件的所有登记对象
        OnNewMail(e)
    }
}

编译器如何实现事件

  • C#编译器会把event事件转化为3个构造:1、一个被初始化为null的私有委托字段 2、一个公共add_xxx方法 3、一个公共remove_xxx方法(P226 1)

设计侦听事件的类型

  • 定义一个类型来使用另一个类型提供的事件(P228 last)
internal sealed class Fax
{
    public Fax(MailManager mm)
    {
//向MailManager的NewMail事件登记回调方法
        mm.NewMail +=Faxmsg;
    }
//触发事件时将调用这个方法
    private void FaxMsg(object sender,NewMailEventArgs e)
    {
        console.log.....
    }
//注销事件侦听
    public void Unregister(MailManager mm)
    {
        mm.NewMail -= FaxMsg;
    }
}

显式实现事件

  • 通过显示实现事件来高效率地实现提供大量事件的类
    代码:(P231)(注意一下应该是排版问题,代码中的EventSet类是包含之后所有函数到结尾的,但其他函数并没有缩进导致看起来和EventSet类同级了)
  • 委托(delegate)的使用:传送门
  • 博客参考:传送门(原文代码中用了线程等内容,会在后续章节中讲述,目前看不太明白的可以看这篇博客)

第十二章 泛型

  • 泛型(generic)是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。(P233 1)即该算法没有设定要操作什么数据类型,因此可以广泛的应用于不同类型的对象,在使用时指定算法要操作的具体数据类型。(P233 2)
  • CLR允许创建泛型引用类型和泛型值类型,不允许创建泛型枚举类型。
    CLR允许创建泛型接口和泛型委托。(P233 3)
  • 泛型的优势:1、源代码保护 2、类型安全 3、更清晰的代码 4、更佳的性能(P235 1)

FCL中的泛型

  • 示例(P237 last)

泛型基础结构

  • 开放类型和封闭类型:具有泛型类型参数的类型仍然是类型,CLR同样会为它创建内部的类型对象。这一点适合引用类型(类)、值类型(结构)、接口类型和委托类型。然而,具有泛型类型参数的类型称为开放类型,CLR禁止构造开放类型的任何实例。这类似于CLR禁止构造接口类型的实例。代码引用泛型类型时可指定一组泛型类型实参。为所有类型参数都传递了实际的数据类型,类型就成为封闭类型。CLR允许构造封闭类型的实例。(P239 1)
  • 泛型类型和继承:泛型也是类型,可以从任何类型派生。指定类型的实参不影响继承层次结构(用于判断强制类型是否被允许)。(P240 last)
  • 泛型类型同一性:如果有一类继承自泛型,并分别实例化两个对象,那么泛型和类的对象在使用==符号判等时返回的结果为FALSE。可以使用using指令来化简泛型封闭类型,例
using DateTimeList = system.collections.Generic.List<System.DateTime>;
  • 代码爆炸:使用泛型类型参数的方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码(这些代码为操作指定数据类型“量身定制”)。这正是你希望的,也是泛型的重要特点。但这样做有一个缺点:CLR要为每种不同的方法/类型组合生成本机代码。我们将这个现象称为代码爆炸。它可能造成应用程序的工作集显著增大,从而损害性能。(P243 1)
    优化:相同类型实参调用只需组合编译一次。(P243 2)CLR判定所有引用类型实参都完全相同则可以代码共享。(P243 3)

泛型接口

  • 没有泛型接口,每次用非泛型接口(如IComparable)来操纵值类型都会发生装箱,而且会失去编译时的类型安全性。这将严重制约泛型类型的应用范围。因此,CLR提供了对泛型接口的支持。引用类型或值类型可指定类型实参实现泛型接口。也可保持类型实参的未指定状态来实现泛型接口。(P243 last)

泛型委托

  • CLR支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。此外,泛型委托允许值类型实例在传给回调方法时不进行任何装箱。(17章会详细讲述)

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

  • 委托的每个泛型类型参数都可标记为协变量(convariant)或逆变量(contravariant)。利用这个功能,可将泛型委托类型的变量转换为相同的委托类型(但泛型参数类型不同)。泛型类型参数可以是:不变量,逆变量(泛型类型参数可以从一个类更改为它的某个派生类,用in标记),协变量(泛型类型参数可以从一个类更改为它的某个基类,用out标记)。

泛型方法

  • 定义泛型类、结构或接口时,类型中定义的任何方法都可引用类型指定的类型参数。类型参数可作为方法参数、方法返回值或方法内部定义的局部变量的类型使用。然而,CLR还允许方法指定它自己的类型参数。这些类型参数也可作为参数、返回值或局部变量的类型使用。(P247 1)
  • 泛型方法和类型推断:C#编译器支持调用泛型方法时进行类型推断。(P248 last3)类型可以定义多个方法,让其中一个方法接受具体数据类型,让另一个接受泛型类型参数。(P249 2)

泛型和其他成员

  • 在C#中,属性、索引器、事件、操作符方法、构造器和终结器本身不能有类型参数。但它们能在泛型类型中定义,而且这些成员中的代码能使用类型的类型参数。(P249 last2)

可验证性和约束

  • 约束的作用:限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型执行更多操作。(P250 last3)
    约束可应用于泛型类型的类型参数,也可应用于泛型方法的类型参数(如下所示)。CLR不允许基于类型参数名称或约束来进行重载,只能基于元数(类型参数个数)对类型或方法进行重载。(P251 3)
//C#的 where关键字告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型lComparable 接口。
//有了这个约束﹐就可以在方法中调用CompareTo,因为已知IComparable<T>接口定义了CompareTo。
private static T Min<T>(T o1,T o2) where T : IComparable<T>
{
    if(o1.CompareTo(o2) < 0) return o1;
    else return o2;
}
  • 主要约束:类型参数可以指定零个或者一个主要约束。主要约束可以是代表非密封类的一个引用类型。不能指定以下特殊引用类型:System.Object ,System.Array,System.Delegate ,System.MulticastDelegate,System.ValueType,System.Enum或者System.Void。(P252 4)
    可以这是class和struct为主要约束(P252 last3)
  • 次要约束:类型参数可以指定零个或者多个次要约束,次要约束代表接口类型。这种约束向编译器承诺类型实参实现了接口。由于能指定多个接口约束,所以类型实参必须实现了所有接口约束(以及主要约束,如果有的话)。第十三章将详细讨论接口约束。(P25 3)
  • 构造器约束:类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。注意,如果同时使用构造器约束和 struct约束,C#编译器会认为这是一个错误,因为这是多余的;所有值类型都隐式提供了公共无参构造器。(where T :new())(P254 2)
  • 其他可验证性问题:(P254 last)
    1、泛型类型变量的转型:将泛型类型的变量转型为其他类型是非法的,除非转型为与约束兼容的类型。
    2、将泛型类型变量设置为默认值:将泛型类型变量设为null是非法的,除非将泛型类型约束成引用类型。
    3、将泛型类型变量与null进行比较:无论泛型类型是否被约束,使用–或!=操作符将泛型类型变量与null进行比较都是合法的:
    4、两个泛型类型变量相互比较:如果泛型类型参数不能肯定是引用类型,对同一个泛型类型的两个变量进行比较是非法的。
    5、泛型类型变量作为操作数使用:会出大量问题,建议不用。

LEAVE A COMMENT