点奈特C#学习记录

#.NET 共 7,924 字 约 25 分钟

前记

作为平时的学习记录,内容大多来自微软官方文档以及《C#图解教程》(第五版)。

2020.8.28

C#字符串

1.字符串内插
在字符串左引号前添加$,可以在大括号 之间的字符串内包括变量。例如

plaintext
UTF-8|1 Line|
Console.WriteLine($"Hello {aFriend}");

2.几个方法
TrimStart、TrimEnd删除空格
Replace替换
Contains搜索包含 StartWith、EndWith搜索开头结尾

VisualStudio基础使用

  • 创建.NETCore控制台应用程序,添加断点、条件断点调试,F11单步调试程序。
  • 使用“即时窗口”,以交互方式更改变量值。
  • 发布应用创建的文件
    x.deps.json 运行时依赖项文件 x.runtimeconfig.json 运行时配置文件
    x.dll 依赖于框架的部署版本,执行此动态链接库可用
plaintext
UTF-8|1 Line|
dotnet x.dll
  • 创建.NETStandard库
    编写类库项目,生成解决方案验证正确与否。添加控制台应用,将类库项目添加到应用依赖项引用。
  • 对库进行单元测试
    新建MStest测试项目,添加被测项目的引用。写完保存时选择“保存时使用编码”,选择“Unicode (UTF-8 带签名) - 代码页 65001”,即可测试。
  • Assert断言常用方法

QQ截图20200829211204.png

  • 安装和使用NuGet包
    安装略,选择“工具”——“NuGet包管理器”——“包管理器控制台”,在DefaultProject选择项目,输入命令
plaintext
UTF-8|1 Line|
Install-Package Newtonsoft.Json

QQ截图20200828160448.png 项目中使用using引进包,JsonConvert.SerializeObject方法将对象转换为可人工读取的字符串。

控制台应用Calculator

  • 修复被零除的错误
  • NaN(Not a Number),用于处理计算出现的错误情况,例如0.0/0.0或者求负数的平方根。
  • 分为两个类 Calculator负责计算,Program类处理用户界面和错误捕获
  • 新建项目“类库”,并在原项目中添加对其引用,将之前Calculator类及其代码剪切到类库CalculatorLibrary的命名空间里。注意,需要使Calculator类public从而在库外部公开它才可使用。
  • 写入日志:使用Trace类,引入System.Diagnostics包。添加构造函数
plaintext
UTF-8|8 Lines|
public Calculator()
     {
         StreamWriter logFile = File.CreateText("calculator.log");
         Trace.Listeners.Add(new TextWriterTraceListener(logFile));
         Trace.AutoFlush = true;
         Trace.WriteLine("Starting Calculator Log");
         Trace.WriteLine(String.Format("Started {0}", System.DateTime.Now.ToString()));
     }

把之前用来计算的静态方法改为成员方法,在每个计算后添加写入日志的代码。运行程序后,在文件目录下找到calculator.log。

  • 写入JSON文件
    添加NuGet包Newtonsoft.json,创建JsonWriter成员对象,改写构造函数,最后添加一种方法从而完成JSON语法
plaintext
UTF-8|6 Lines|
public void Finish()
 {
     writer.WriteEndArray();
     writer.WriteEndObject();
     writer.Close();
 }

简单的创建WPF应用程序

简单的创建Windows窗体应用程序

与之前的简单安卓app类似。

2020.9.2

简单的窗体应用

计时数学测验没啥好说的。
匹配游戏MatchingGames

  • 添加了游戏计时器,System.Windows.Forms命名空间下的Timer控件,和所在的Form属于同一个进程。Timer控件只有绑定了Tick事件和设置Enabled属性为True周才会自动计时。
  • 添加了音效,使用System.Media命名空间。

字符串内插的高级方案

  • 在内插字符串中使用转义序列
    若要逐字解释转义序列,可使用逐字字符串文本。 内插逐字字符串以 $ 字符开头,后跟 @ 字符。 从 C# 8.0 开始,可以按任意顺序使用 $ 和 @ 标记:$@”…” 和 @$”…” 均为有效的内插逐字字符串。
plaintext
UTF-8|7 Lines|
var userName = "pyq";
var stringWithEscapes = $"C:\\Users\\{userName}\\Documents";
var verbatimInterpolated = $@"C:\Users\{userName}\Documents";
Console.WriteLine(stringWithEscapes);
Console.WriteLine(verbatimInterpolated);
// C:\Users\pyq\Documents
// C:\Users\pyq\Documents
  • 使用字符串插值创建区域性特定的结果字符串
    默认情况下,内插字符串将 CultureInfo.CurrentCulture 属性定义的当前区域性用于所有格式设置操作。使用内插字符串到System.FormattableString 实例的隐式转换,并调用它的 ToString(IFormatProvider) 方法来创建区域性特定的结果字符串。
plaintext
UTF-8|22 Lines|
var cultures = new System.Globalization.CultureInfo[]
{
    System.Globalization.CultureInfo.GetCultureInfo("en-US"),
    System.Globalization.CultureInfo.GetCultureInfo("en-GB"),
    System.Globalization.CultureInfo.GetCultureInfo("nl-NL"),
    System.Globalization.CultureInfo.InvariantCulture
};

var date = DateTime.Now;
var number = 31_415_926.536;
FormattableString message = $"{date,20}{number,20:N3}";
foreach (var culture in cultures)
{
    var cultureSpecificMessage = message.ToString(culture);
    Console.WriteLine($"{culture.Name,-10}{cultureSpecificMessage}");
}

// Expected output is like:
// en-US       5/17/18 3:44:55 PM      31,415,926.536
// en-GB      17/05/2018 15:44:55      31,415,926.536
// nl-NL        17-05-18 15:44:55      31.415.926,536
//            05/17/2018 15:44:55      31,415,926.536

索引和范围

0 索引与 sequence[0] 相同。 ^0 索引与 sequence[sequence.Length] 相同。 表达式 sequence[^0] 不会引发异常,就像 sequence[sequence.Length] 一样。 对于任何数字 n,索引 ^n 与 sequence[sequence.Length - n] 相同。

plaintext
UTF-8|13 Lines|
string[] words = new string[]
{
                // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};              // 9 (or words.Length) ^0

类型系统

CTS(通用类型系统)中值类型和引用类型 QQ截图20200902170229.png

  • 值类型:分为结构和枚举。变量直接包含其值,在声明变量的任何上下文中内联分配内存,没有单独的堆分配或垃圾回收开销。

  • 引用类型:类、委托、数组或接口。
    声明变量时一直包含值null,直到new对象或分配其他对象。创建对象后,内存会在托管堆上进行分配,并且变量只保留对对象位置的引用。

  • 装箱boxing&取消装箱unboxing
    装箱:值类型➡引用类型
    拆箱:引用类型➡值类型

plaintext
UTF-8|3 Lines|
int i = 123;      // a value type
object o = i;     // boxing
int j = (int)o;   // unboxing

QQ截图20200902183902.png

装箱时生成的是全新的引用对象,会有时间损耗,造成效率降低。所以应尽量避免装箱。一般可通过重载函数和泛型避免。

委托(Delegate)

建议不要直接在全局命名空间中声明委托类型(或其他类型)。

委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。
使用委托可以将多个方法绑定到同一个委托变量,当调用此变量时(这里用“调用”这个词,是因为此变量代表一个方法),可以依次调用所有绑定的方法。
声明和定义委托分为4个步骤:

1.声明一个委托(函数指针)
2.创建委托对象
3.创造符合委托格式的函数。(指针指向的函数)
4.将函数名称赋值给委托。

更详细的 解释和实例

事件Event

事件是后期绑定机制,事件是对象用于(向系统中的所有相关组件)广播已发生事情的一种方式。 任何其他组件都可以订阅事件,并在事件引发时得到通知。
定义事件

plaintext
UTF-8|1 Line|
public event EventHandler<FileLisArgs> Progress;

引发事件时,使用委托调用语法调用事件

plaintext
UTF-8|2 Lines|
Process?.Invoke(this, new FileLisArgs(file));
//?. 运算符可确保在事件没有订阅服务器时不引发事件。

使用 += 运算符订阅事件, -=取消订阅。

9.3

变量

  • c#中不管嵌套级别如何,都不能在第一个名称的有效范围内声明另一个同名的局部变量。

参数

值参数

plaintext
UTF-8|19 Lines|
class Program
        {
            static void MyMethod(MyClass f1, int f2)
            {
                f1.Val = f1.Val + 5;
                f2 = f2 + 5;
                Console.WriteLine($"f1.Val:{f1.Val}, f2:{f2}");
            }

            static void Main()
            {
                MyClass a1 = new MyClass();
                int a2 = 10;
                MyMethod(a1, a2);
                Console.WriteLine($"a1.Val:{a1.Val},a2:{a2}");
            }
        }
//f1.Val:25, f2:15
//a1.Val:25, a2:10
  • 方法开始前,用作实参的变量a2已经在栈里了
  • 方法开始时,系统在栈里为形参分配空间,并从实参复制值。
    1、a1是引用类型,所以引用被复制,结果实参和形参引用堆中的同一个对象(也就是数据20)
    2、a2是值类型,所以值被复制,产生一个独立的数据项
  • 方法结尾,f2和对象f1的字段都被加上了5
    1、执行结束后,形参从栈中弹出
    2、a2值类型,其值不受方法行为的影响 3、a1引用类型,其值被方法行为影响

引用参数

  • 使用时,必须在声明和调用时都使用ref修饰符
  • 实参必须是变量,在用作实参前必须被赋值。若是引用类型变量,可赋值为引用或null。
  • 不会在栈上为形参分配内存
  • 形参的参数名将作为实参变量的别名,指向相同的内存位置
    将上例f1 f2修改为引用参数后,输出为
plaintext
UTF-8|2 Lines|
//f1.Val:25, f2:15
//a1.Val:25, a2:15
  • 方法调用前,将要被用作实参的变量a1和a2已经在栈里了
  • 在方法的开始,形参名被设置为实参的别名,a1和f1引用相同的内存位置,a2和f2亦然。
  • 在方法结束位置,f1的对象的字段和f2都被加上了5
  • 方法执行后,形参名称失效,但是值类型a2的值和引用类型a1所指向的对象的值都被方法内的行为改变了

引用类型作为值参数和引用参数

  • 作为值类型传递:如果在方法内创建一个新对象并赋值给形参,将切断形参与实参的关联,并且方法结束后,新对象也不复存在。
plaintext
UTF-8|27 Lines|
class MyClass
        {
            public int Val = 20;
        }

        class Program
        {
            static void RefAsParameter(MyClass f1)
            {
                f1.Val = 50;
                Console.WriteLine($"After member assignment: {f1.Val}");
                f1 = new MyClass();
                Console.WriteLine($"After new object creation: {f1.Val}");
            }

            static void Main()
            {
                MyClass a1 = new MyClass();
                Console.WriteLine($"Before Method call: {a1.Val}");
                RefAsParameter(a1);
                Console.WriteLine($"After method call: {a1.Val}");
            }
        }
//Before Method call: 20
//After member assignment: 50
//After new object creation: 20
//After method call: 50
  • 作为引用参数:如果在方法内创建一个新对象并赋值给形参,在方法结束后该对象依然存在,并且是实参引用的值。
    在上例代码基础上,添加ref关键字,输出为
plaintext
UTF-8|4 Lines|
Before Method call: 20
After member assignment: 50
After new object creation: 20
After method call: 20

输出参数

和引用参数不同的地方

  • 在方法内部,给输出参数赋值之后才能读取它。也就是在方法调用前没有必要给实参赋值。
  • 在方法内部,在方法返回之前,代码中每条可能的路径都必须为所有输出参数赋值。
  • 从C#7.0开始,可以在调用方法时在参数列表中添加一个变量类型,以其作为变量声明
plaintext
UTF-8|12 Lines|
//原来的写法
 static void Main()
            {
                MyClass a1 = new MyClass();
                int a2;
                MyMethod(out a1, out a2);
            }
//新的写法
static void Main()
            {
                MyMethod(out MyClass a1, out int a2);
            }

参数数组

  • 一个参数列表只能有一个参数数组
  • 若有,必须是列表中的最后一个
  • 所表示的所有参数必须是同一类型
  • params修饰符:在声明时需要,调用时不允许有。
  • 数组在堆中被创建时,实参的值被复制到数组中。
  • 如果数组参数时值类型,则值被复制,实参在方法内部不受影响;如果数组参数是引用类型,则引用被赋值,实参引用的对象在方法内部会受影响。

9.5

属性

  • 属性和后备字段的命名约定。
plaintext
UTF-8|3 Lines|
    private int firstField;
    //或写成_firstField
    public  int FirstField;
  • get/set访问器至少有一个必须被定义,否则编写会出错。
  • 属性比公有字段更好:
    1、是函数成员而非数据成员,允许处理输入和输出。
    2、可以只读或只写
    3、编译后的变量和编译后的属性语义不同

readonly修饰符

  • const字段只能在声明语句中初始化,readonly除此之外还可以在类的任何构造函数设置。
  • const字段的值必须可在编译时决定,readonly字段的值可以在运行时决定。这使得允许在不同环境或不同构造函数中设置不同的值。
  • const行为总是静态的,而readonly字段:可以是实例字段也可以是静态字段;在内存中有存储位置。

索引器

  • 可以认为索引器是为类的多个数据成员提供get和set访问的属性
  • 索引器没有名称,用this。

分部方法

  • 返回类型必须是void
  • 签名不能包括访问修饰符,这使得分部方法是隐式私有的
  • 参数列表不能包含out参数
  • 在定义和声明中必须在void前添加partial

9.6

类和继承

屏蔽

  • 屏蔽基类的成员:在派生类中声明一个相同名称相同类型的成员。要让编译器知道是故意屏蔽的话,可使用new修饰符,否则会警告隐藏了继承的成员。
  • 签名由名称和参数列表组成,不包括返回类型。

覆写

  • 覆写和被覆写必须有相同的可访问性;
  • 方法、属性、索引器和事件都可以被声明为virtual和override;
  • static和非虚方法不能覆写。
  • 当使用对象基类部分的引用调用一个被覆写的方法时,方法的调用被沿派生层次上溯执行,一直到标记为override的方法的最高派生版本。

析构函数执行

  • 初始化实例成员→调用基类构造函数→执行实例构造函数的方法体
  • 如果派生类使用一个指定的基类构造函数而不是无参数构造函数,必须在构造函数初始化语句中指定它
    1、使用base并指明使用哪一个基类构造函数
plaintext
UTF-8|3 Lines|
    public MyDerivedClass(int x, string s):base(s,x){
        ……
    }

2、使用this并指明使用当前类的哪一个构造函数

plaintext
UTF-8|3 Lines|
    public MyClass(int x):this(x,"using default string"){
        ……
    }

这种语法的好处是,如果一个类有好几个构造函数,并且都需要在对象构造过程开始时执行一些公共的代码,就可以提取公共代码作为一个公共构造函数(设置为public),被其他所有的构造函数用作构造函数初始化语句。

程序集间的继承

  • 增加对其他程序集的引用是告诉编译器所需的类型在哪里定义。
  • 增加using指令允许你引用其他的类而不必使用它们的完全限定名称。

成员访问修饰符

五种访问级别如图所示 蜂蜜浏览器_IMG_20200906_111340.jpg

QQ截图20200906112308.png

扩展方法

  • 声明扩展方法的类必须声明为static
  • 扩展方法本身必须声明为static
  • 必须包含关键字this作为它的第一个参数类型
plaintext
UTF-8|5 Lines|
    static class ExtendMyData{
        public static double Average(this MyData md){
            ……
        }
    }

表达式和运算符

字面量

  • 无后缀的实数字面量是double类型
  • 字符字面量的不同格式
plaintext
UTF-8|5 Lines|
    char c1 = "d";//单个字符
    char c2 = "\n";//简单转义序列
    char c3 = "\x0061";//十六进制转义序列
    char c4 = "\u005a";//Unicode转义序列
    

9.7

表达式和运算符

比较操作

  • 对大多数引用类型来说,只比较他们的引用(浅比较):如果指向内存中相同的对象则返回true,否则false。即使内存中两个分离的对象在所有其他地方都是完全相等的
  • string类型比较相等性时,会比较长度和内容,区分大小写,即深比较。

条件逻辑运算符

使用“短路”模式操作,如果计算Expr1之后结果已经确定了,将会跳过Expr2的求值。因此不要在Expr2中放置带副作用的表达式(比如改变一个值),因为可能不会计算。

移位运算符

  • 当操作数时有符号整数时,如果最左边的位是1(标志一个负数),在左边移开的位置用1而不是0填充。
  • 对于正数或无符号数,左边移开的位置用0填充。
  • 把整数左移一位的结果与把它乘以2的结果相同,右移一位的结果和除以2相同。

用户定义的类型转换

  • 隐式转换:决定在特定上下文中使用特定类型时,如有需要,编译器会自动执行转换。转换运算符是implicit。
plaintext
UTF-8|4 Lines|
    public static implicit operator TargetType(SourceType Identifier){
        ……
        return ObjectOfTargetType;
    }
  • 显式转换:编译器只在使用显式转换运算符时才执行转换。转换运算符explicit。
plaintext
UTF-8|4 Lines|
    public static explicit operator TargetType(SourceType Identifier){
        ……
        return ObjectOfTargetType;
    }

使用explicit运算符时,需要显式使用转换运算符,也就是把想要把表达式转换成的目标类型的名称组成,放在一对圆括号内部。

运算符重载

  • 一元运算符的重载方法带一个单独的class或struct类型的参数
  • 二元运算符的重载方法带两个参数,其中至少有一个必须是class或struct类型。
  • 重载的方法声明必须同时使用public和static
  • 运算符必须是要操作的类或结构的成员

typeof运算符

  • 返回作为其参数的任何类型的System.Type对象,可以了解类型的特征。对于任何已知类型,只有一个System.Type对象。
plaintext
UTF-8|23 Lines|
class SomeClass
    {
        public int Field1;
        public int Field2;

        public void Method1() { }
        public int Method2() { return 1; }
    }

    class Program
    {
        static void Main()
        {
            Type t = typeof(SomeClass);
            FieldInfo[] fi = t.GetFields();
            MethodInfo[] mi = t.GetMethods();

            foreach (FieldInfo f in fi)
                Console.WriteLine($"Field: {f.Name}");
            foreach (MethodInfo m in mi)
                Console.WriteLine($"Method: {m.Name}");
        }
    }

输出结果为

plaintext
UTF-8|8 Lines|
Field: Field1
Field: Field2
Method: Method1
Method: Method2
Method: GetType
Method: ToString
Method: Equals
Method: GetHashCode

GetType方法也会调用typeof运算符,该方法对每个类型的每个对象都有效。

9.8

using语句

第一种形式:括号内代码分配资源;statement时使用资源的代码;using语句隐式产生处置资源的代码。

plaintext
UTF-8|1 Line|
using(ResourceType Identifier = Expression)Statement

它会执行这些任务:

  • 分配资源
  • 把statement放进try块
  • 创建资源的Dispose方法的调用,并放进finally块

整个结构被封闭在一个隐式的块里,当finally块退出时,资源失效且不会被意外调用。

第二种形式(不推荐使用

plaintext
UTF-8|2 Lines|
using(Expression) EmbeddedStatement
        资源           使用资源

数组

基础

  • C#不支持动态数组
  • 数组实例是从System.Array继承类型的对象
  • 和其他局部变量一样,数组也可以是隐式类型

交错数组

  • 不能在声明语句中初始化顶层数组之外的数组
plaintext
UTF-8|2 Lines|
    int[][] jarArr = new int[3][]; //正确
    int[][] jarArr = new int[3][4];//编译错误
  • 实例化完整的交错数组
plaintext
UTF-8|5 Lines|
    int[][] Arr = new int[3][];//实例化顶层数组
    
    Arr[0] = new int[]{10,20,30};//实例化子数组
    Arr[1] = new int[]{40,50,60,70};
    Arr[2] = new int[]{80,90,100,110,120};

foreach

  • 迭代变量是只读的,不能改变。对于引用类型数组来说,虽仍不能改变引用,但是可以通过迭代变量改变数据。
plaintext
UTF-8|17 Lines|
    Class MyClass{
        public int MyField = 0;
    }
    
    Class Program{
        static void Main(){
            MyClass[] mcArray = new MyClass[4];
            for(int i = 0; i < 4; i++){
                mcArray[i] = new MyClass[4];
                mcArray[i].MyField = i;
            }
            foreach(myClass item in mcArray)
                item.MyField += 10;
            foreach(MyClass item in mcArray)
                Console.WriteLine($"{item.Field}"));
        }
    }

输出为

plaintext
UTF-8|4 Lines|
10
11
12
13

数组协变

在某些情况下,即使某个对象不是数组的基类型,也可以把它赋值给数组元素。需要符合如下条件:

  • 是引用类型数组
  • 在赋值的对象类型和数组基类型之间有隐式转换或显式转换

因此总是可以将一个派生类的对象赋值给基类声明的数组。

Clone方法

  • 克隆值类型数组会产生两个独立数组
  • 克隆引用类型数组会产生指向相同对象的两个数组
  • Clone方法返回Object类型的引用,必须被强制转换成数组类型

9.10

VB基本结构

简单的示例

plaintext
UTF-8|29 Lines|
Imports System

Public Class Rectangle
    Private length As Double
    Private width As Double

    Public Sub AcceptDetails()
        length = 4.5
        width = 3.5
    End Sub

    Public Function GetArea() As Double
        GetArea = length * width
    End Function

    Public Sub Display()
        Console.WriteLine("Length:{0}", length)
        Console.WriteLine("Width:{0}", width)
        Console.WriteLine("Area:{0}", GetArea())
    End Sub

    Shared Sub Main()
        Dim r As New Rectangle
        r.AcceptDetails()
        r.Display()
        Console.ReadLine()
    End Sub

End Class

一个简单VB程序主要的组成部分

  • 命名控件声明 Namespace Declaration
  • 一个类或模块 A class or module
  • 一个或多个程序 One or more procedures
  • 变量 Variables
  • 主过程 The Main procedure
  • 语句和表达式 Statement & Expressions
  • 注释 Comments

9.14

委托(补充)

概述

  • 声明委托(类型)——>声明委托类型的变量——>创建委托实例并把其引用赋值给变量,增加第一个方法——>调用委托对象
  • 把委托delegate看作一个方法列表,这些方法具有相同的签名和返回类型。

Lambda表达式

plaintext
UTF-8|21 Lines|
    delegate double MyDel(int par);
    class Program
    {
        static void Main()
        {
           
            MyDel del = delegate (int x) { return x + 1; };//匿名方法

            MyDel le1 = (int x) => { return x + 1; };//Lambda表达式
            MyDel le2 = (x) => { return x + 1; };
            MyDel le3 = x=> { return x + 1; };
            MyDel le4 = x => x + 1;

            Console.WriteLine($"{del(12)}");
            Console.WriteLine($"{le1(12)}");
            Console.WriteLine($"{le2(12)}");
            Console.WriteLine($"{le3(12)}");
            Console.WriteLine($"{le4(12)}");
        }
    }
    //输出都是13
  • 表达式参数列表的参数必须和委托相匹配(数量、类型、位置)
  • 不一定需要包含类型(隐式),除非委托有ref或out参数,此时必须注明类型(显式)
  • 如果只有一个参数,且隐式,则两端圆括号可以省略
  • 如果没有参数,必须使用一组空的圆括号

事件

发布pub/订阅sub模式

这是一种经常使用到的设计模式,sub会监听一类消息来达到pub发布的时候进行相应的处理。
看到网上有人把它和观察者模式比较 QQ截图20200914101709.png

从表面上看:

观察者模式里,只有两个角色 —— 观察者 + 被观察者 而发布订阅模式里,还有一个—— 经纪人Broker

往更深层次讲:

观察者和被观察者,是松耦合的关系 发布者和订阅者,则完全不存在耦合

但是实际上,《设计模式》这本书讲观察者模式对复杂情况下如何处理进行了描述:

对于不同观察者需要关心不同的变化时需要加入ObserverManager进行复杂情况的处理。观察者将被观察时间在ObserverManager中进行注册,事件发生变更时在ObserverManager中寻找已注册该事件的观察者,将消息发送给对应的观察者。

ObserverManager就是Broker的角色,其实两个没有本质性的区别。

源代码组件概览

需要在事件中使用的代码有五部分

  • 委托类型声明
  • 事件处理成声明
  • 事件声明
  • 事件注册
  • 触发事件的代码

声明事件

  • 事件声明在一个类里
  • 需要委托类型的名称,任何附加到事件的处理程序都必须与委托类型的签名和返回类型匹配
  • 声明为public
  • 不能用new创建其对象
plaintext
UTF-8|5 Lines|
    class Incrementer{
        public event EventHandler CountedADozen;
              关键字  委托类型       事件名
        ……
    }
  • 事件是成员,并不是类型。

订阅事件

使用+=运算符添加事件处理程序,事件处理程序的规范可以是以下任意一种

  • 实例方法的名称
  • 静态方法的名称
  • 匿名方法
  • Lambda表达式

触发事件

  • 在触发之前和null进行比较,从而查看事件是否包含事件处理程序
  • 触发事件的语法和调用方法的相同

完整示例

plaintext
UTF-8|46 Lines|
    delegate void Handler();

//发布者pub
    class Incrementer
    {
        public event Handler CountedADozen;
    //public event EventHandler CountedADozen;
        public void DoCount()
        {
            for(int i = 1; i < 100; i++)
            {
                if (i % 12 == 0 && CountedADozen != null)
                    CountedADozen();
                    //CountedADozen(this,null);
            }
        }
    }
//订阅者sub
    class Dozens
    {
        public int DozensCount { get; private set; }

        public Dozens(Incrementer incrementer)
        {
            DozensCount = 0;
            incrementer.CountedADozen += IncrementDozensCount;
//void IncrementDozensCount(object source, EventArgs e)
            void IncrementDozensCount()
            {
                DozensCount++;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            Incrementer incrementer = new Incrementer();
            Dozens dozensCounter = new Dozens(incrementer);

            incrementer.DoCount();
            Console.WriteLine("Number of dozens = {0}", dozensCounter.DozensCount);
        }
    }
    //输出:Number of dozens = 8

标准事件的用法

System命名空间中声明的EventHandler委托类型
首先上例中注释掉的部分,就是改为使用EventHandler委托的写法。
关于委托的声明:

plaintext
UTF-8|1 Line|
    public delegate void EventHandler(object sender, EventArgs e);
  • 第一个参数用来保存触发事件的对象的引用。是object类型的,所以可以匹配任何类型实例
  • 第二个参数保存状态信息,指明什么类型适用。
  • 返回类型是void

这其中第二个参数是EventArgs类的对象,但是

  • EventArgs不能传递任何数据
  • 如果要传数据,必须声明一个派生自EventArgs的类,并使用合适字段保存需要传递的数据。

object和EventArgs类型的参数总是基类,这样EventHandler就可以提供一个对所有事件和事件处理器都通用的签名。

通过扩展EventArgs来传递数据

对上面的实例进行修改扩展

plaintext
UTF-8|64 Lines|
    delegate void Handler();
    
    public class IncrementerEventArgs: EventArgs
    {
        public int IterationCount { get; set; }
    }
    class Incrementer
    {
        public event EventHandler<IncrementerEventArgs> CountedADozen;
        public void DoCount()
        {
            IncrementerEventArgs args = new IncrementerEventArgs();
            for(int i = 1; i < 100; i++)
            {
                if (i % 12 == 0 && CountedADozen != null)
                {
                    args.IterationCount = i;
                    CountedADozen(this, args);
                }
                    
            }
        }
    }

    class Dozens
    {
        public int DozensCount { get; private set; }

        public Dozens(Incrementer incrementer)
        {
            DozensCount = 0;
            incrementer.CountedADozen += IncrementDozensCount;

            void IncrementDozensCount(object source,IncrementerEventArgs e)
            {
                Console.WriteLine($"Incremented at iteration :{e.IterationCount}in {source.ToString()}");
                DozensCount++;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            Incrementer incrementer = new Incrementer();
            Dozens dozensCounter = new Dozens(incrementer);

            incrementer.DoCount();
            Console.WriteLine("Number of dozens = {0}", dozensCounter.DozensCount);
        }
    }
        
}
输出:
Incremented at iteration :12in demo1.Incrementer
Incremented at iteration :24in demo1.Incrementer
Incremented at iteration :36in demo1.Incrementer
Incremented at iteration :48in demo1.Incrementer
Incremented at iteration :60in demo1.Incrementer
Incremented at iteration :72in demo1.Incrementer
Incremented at iteration :84in demo1.Incrementer
Incremented at iteration :96in demo1.Incrementer
Number of dozens = 8
  • 首先就是自定义一个派生于EventArgs的类,来保存我们需要传入的数据。它能将字符串储存在名为Message的字段里。
  • 需要一个使用自定义类的委托类型,此处使用泛型版本的EventHandler<>
  • 后面相应的自定义类对象,并在触发事件时传递参数。
移除事件处理程序
  • 在使用完事件处理程序之后,可以用-=将其移除
  • 如果一个处理程序向事件注册了多次,那执行移除的时候,将只移除列表中该处理程序的最后一个实例

接口

使用IComparable接口的示例

Array类的Sort()方法依赖于一个叫做IComparable的接口,声明在BCL(基类库Base Class Library)中,包含唯一的方法CompareTo。

plaintext
UTF-8|3 Lines|
    public interface IComparable{
        int CompareTo(object obj);
    }

在IComparable接口的声明中,包含Compare方法的声明,但是没有实现,实现用分号表示。但是. NET文档中描述了该方法应该做的事情。

文档中写道,在调用该方法时,它应该返回以下几个值之一:

  • 负数值:如果当前对象小于参数对象
  • 正数值:如果当前对象大于参数对象
  • 零 :如果两个对象相等

int类型实现了IComparable,因此sort方法可以直接排序,但是自己定义的类数组并没有实现IComparable所以直接使用sort的话会出现异常。于是可以让自己的类实现IComparable接口

plaintext
UTF-8|10 Lines|
    class MyClass:IComparable{
        public int TheValue;
        
        public int CompareTo(object obj){
            MyClass mc = (MyClass)obj;
            if(this.TheValue < mc.TheValue)return -1;
            if(this.TheValue > mc.TheValue)return 1;
            return 0;
        }
    }

这样就可以使用sort排序MyClass对象数组了,完整代码如下。

plaintext
UTF-8|41 Lines|
    class MyClass : IComparable
    {
        public int TheValue;

        public int CompareTo(object obj)
        {
            MyClass mc = (MyClass)obj;
            if (this.TheValue < mc.TheValue) return -1;
            if (this.TheValue > mc.TheValue) return 1;
            return 0;
        }
    }

    class Program
    {
        static void PrintOut(string s, MyClass[] mc)
        {
            Console.Write(s);
            foreach (var m in mc)
                Console.Write($"{m.TheValue} ");
            Console.WriteLine(" ");
        }

        static void Main()
        {
            var myInt = new[] { 20, 4, 16, 9, 2 };

            MyClass[] mcArr = new MyClass[5];
            for(int i = 0; i < 5; i++)
            {
                mcArr[i] = new MyClass();
                mcArr[i].TheValue = myInt[i];
            }
            PrintOut("Initial Order: ", mcArr);
            Array.Sort(mcArr);
            PrintOut("Sorted Order: ", mcArr);
        }
    }
    //输出:
    //Initial Order: 20 4 16 9 2
    //Sorted Order: 2 4 9 16 20

声明接口

  • 接口成员不能有数据成员、静态成员
  • 成员声明不能包含任何实现,用分号代替主体
  • 与类和结构一样,也可以分部声明
  • 接口声明可以有任何的访问修饰符
  • 接口成员是隐式public,不能加其他的,包括public
plaintext
UTF-8|4 Lines|
    interface IMyInterface1{
        int DoStuff(int nVar1, long lVar2);
        double DoOtherStuff(string s, long x);
    }

实现接口

只有类和结构才能实现接口。如前文的Sort所示,要实现接口,类或结构必须:

  • 在基类列表中包括接口名称;
  • 为每一个接口成员提供实现
  • 如果类派生自基类并实现接口,那基类名称必须放在所有接口前面

接口是引用类型

接口是一个引用类型,我们可以通过类对象引用强制转换为接口类型来获取指向接口的引用。一旦有了引用,就可以使用点语法来调用接口的成员。

plaintext
UTF-8|2 Lines|
IIfc ifc = (IIfc1)mc;//获取接口的引用
ifc.PrintOut("interface");//使用接口的引用调用方法

as运算符

  • 如果类实现了接口,表达式返回指向接口的引用
  • 如果类没有实现接口,表达式返回null而不是抛出异常
plaintext
UTF-8|3 Lines|
ILiveBirth b = a as IliveBirth;//跟cast:(ILiveBirth)a一样
if(b!=null)
Console.WriteLine($"Baby is called:{b.BabyCalled()}");

访问显式接口成员实现

需要强制转换当前对象的引用(this)为接口类型的引用,并使用这个指向接口的引用来调用显式接口实现。

plaintext
UTF-8|12 Lines|
    class MyClass: IIfc1{
        void IIfc1.PringOut(string s){
            Console.WriteLine("IIfc1");
        }
        
        public void Method1(){
            PrintOut("...");//编译错误
            this.PringOut("...");//编译错误
            
            ((IIfc1)this).PrintOut("...");//正确调用
        }
    }