# 高级C#技术

# ::运算符

如果一个名称空间的别名与实际名称空间层次结构之间的界限不清晰,在这种情况下,名称空间层次结构优先于名称空间别名,要使用名称空间别名就必须使用::运算符。看下面的例子:

using MyAlias = LearnCSharp1.MyNestedReal;

namespace MyAlias
{
    public class MyClass
    {
        public MyClass()
        {
            Console.WriteLine("MyClass in global MyAlias!");
        }
    }
}

namespace LearnCSharp1
{
    namespace MyAlias
    {
        public class MyClass
        {
            public MyClass()
            {
                Console.WriteLine("MyClass in MyAlias!");
            }
        }
    }

    namespace MyNestedReal
    {
        public class MyClass
        {
            public MyClass()
            {
                Console.WriteLine("MyClass in MyNestedReal!");
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyAlias.MyClass m1 = new MyAlias.MyClass();
            MyAlias::MyClass m2 = new MyAlias::MyClass();
            global::MyAlias.MyClass m3 = new global::MyAlias.MyClass();
            List<int> l1 = new List<int>();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 定制异常

前面讨论异常的过程中,我们使用的是 .Net标准异常,包括异常的基类System.Exception。有时我们需要定义自己的异常类,这样可以把更具体的信息发送给捕获该异常的代码,让异常处理的更有针对性。System名称空间中有两个基本的异常类:ApplicationException和SystemException,它们派生于Exception。SystemException用作 .Net 预定义的异常基类,而ApplicationException由开发人员用于派生自己的异常类,但是最近好像都是使用Exception。看下面实例:

public class MyError
{
public int code = 10;
}
public class MyException : Exception
{
public string info;
public MyError myError;
public MyException(MyError myError) : base("this is my custom error saved in base!")
{
    this.myError = myError;
    info = "some error detail!";
}
}
static void Main(string[] args)
{
    try
    {
        MyError me = new MyError();
        if (me.code == 10)
            throw new MyException(me);
    }
    catch(MyException e)
    {
        Console.WriteLine(e.Message);
        Console.WriteLine(e.info);
        Console.WriteLine(e.myError.code);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 事件

事件类似于异常,都会先触发然后再处理,但是它并没有try...catch类似的结构来处理事件,你需要订阅(subscribe)它们,也就是提供事件发生时要执行的代码,我们称之为事件处理程序。单个事件可订阅多个处理程序,当事件发生时,这些处理程序就会调用,事件处理程序本身都是简单方法,对事件处理方法的唯一限制就是它必须匹配事件所要求的返回类型和参数,在C#中一般通过委托来实现事件的订阅。来看样例:

class Program
{
    public static int count = 0;

    public static string MyStr = "This is my world!";
    static void Main(string[] args)
    {
        Timer myTimer = new Timer(100);
        myTimer.Elapsed += new ElapsedEventHandler(PrintStr);
        myTimer.Start();
        System.Threading.Thread.Sleep(200);
        Console.ReadKey();
        Console.WriteLine("program end!");
    }
    static void PrintStr(object source,ElapsedEventArgs e)
    {
        Console.Write(MyStr[count++ % MyStr.Length]);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 相关解释

.Net 提供了两个委托类型,EventHandler和EventHandler<T>,以便定义事件,它们使用标准的事件处理模式。注意如果事件不需要事件实参数据,仍然可以使用EventHandler委托类型,可以传递EventArgs.Empty作为实参值。

Timer.Elapsed事件的委托包含了事件处理程序中常见的两类参数:

  • object source —— 引发事件的对象
  • ElapsedEventArgs e —— 由事件传送的参数

由source参数的原因是,我们常常为不同的对象引发相同的事件准备一个事件处理程序,这个参数可以帮助我们确定是哪个对象引发了事件。

前面的所有事件处理函数都使用void类型返回值,实际上这些函数时可以提供返回值的,但是当有多个事件处理函数时,则使用最后一个的事件处理函数的返回值。

可以使用匿名方法(anonymous method)作为事件处理程序,但是注意如果匿名方法处在一个局部作用域时,它引用了该作用域中的一些变量,这些变量会变成外部变量(outer variable),当这个局部作用域被调用后,这些外部变量不会像其他作用域中的变量一样被销毁,只有匿名方法被删除时,这些外部变量才会被释放,注意当外部变量占用很大的内存时,一定要注意这点。

请看下面的实例:

public class MyCustomEventArgs : EventArgs
{
    private string message;
    public string Message
    {
        get
        {
            return message;
        }
    }

    public MyCustomEventArgs()
    {
        message = "No Message!";
    }

    public MyCustomEventArgs(string news)
    {
        message = news;
    }
}

public class EventTriggerClass{
    public event EventHandler<MyCustomEventArgs> MessageHandler;
    Timer myTimer;

    public string Name { get; set; }
    public EventTriggerClass()
    {
        Console.WriteLine("Init Trigger Class!");
        myTimer = new Timer(100);
        myTimer.Elapsed += new ElapsedEventHandler(CheckMessage);
    }

    public void Start()
    {
        myTimer.Start();
    }
    public void CheckMessage(object source,ElapsedEventArgs e)
    {
        Console.WriteLine("---------Enter Check Message Method!----------");
        if(MessageHandler != null)
        {
            MessageHandler(this, new MyCustomEventArgs("hello world!"));
        }
    }
}



class Program
{
    static void InstanceAnotherClass()
    {
        int id = 0;
        EventTriggerClass et2 = new EventTriggerClass();
        et2.Name = "et2 instance";
        et2.MessageHandler += delegate (object source,MyCustomEventArgs e){ 
            Console.WriteLine($"Message Arrived From :{((EventTriggerClass)source).Name}");
            Console.WriteLine("this is my temp method");
            Console.WriteLine(id++);
        };
        et2.Start();

    }
    static void Main(string[] args)
    {
        EventTriggerClass et1 = new EventTriggerClass();
        et1.Name = "et1 instance";
        et1.MessageHandler += MessageHandle;
        et1.Start();
        InstanceAnotherClass();
        Console.ReadKey();

    }
    static void MessageHandle(object source,MyCustomEventArgs e)
    {
        Console.WriteLine($"Message Arrived From :{((EventTriggerClass)source).Name}");
        Console.WriteLine($"Message content:{e.Message}");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

# 特性

特性(attribute)可以为代码提供额外的信息,它为代码标记一些信息,这些信息再被读取,并影响我们所定义类型的使用方式。比如下面:

[DebuggerStepThrough]
public void TestMethod(){

}
1
2
3
4

上面的DebuggerStepThrough特性告诉VisualStudio在调试时不要进入该方法进行逐句调试,而是跳过该方法。这个特性实际上是通过DebuggerStepThroughAttribute这个类来实现的,这个类位于System.Diagnostics名称空间中,如果我们要使用这个特性需要引用这个名称空间,使用该特性既可以直接使用完整的名称又可以像上面那样去掉后缀Attribute。

# 创建特性

通过对System.Attribute类型派生,我们可以创建自己的特性,我们首先决定要把该特性应用到什么类型的目标(类,属性或其他),其次要决定该特性是否对同一个目标进行多次应用。我们可以对自己的特性应用一个特性来实现,要使用的特性是AttributeUsageAttribute,这个特性带有一个类型为AttributeTargets的构造函数参数值,通过|运算符可组合出我们需要的值,该特性还有一个AllowMultiple属性,用于指定是否可以多次应用特性。

# 读取特性

要读取特性,需要使用反射(reflection)技术,它能让我们可以在运行的时候动态检查类型信息,使用反射取得Type对象中的使用信息,以及通过System.Reflection名称空间中的各种类型来获取不同的类型信息。

现在我们使用Type.GetCustomAttributes()方法来获取属性,它的第一个参数可选,传递一个我们感兴趣的特性类型,其他所有特性会被忽略,第二个参数通过一个布尔值来表示是否想了解该类的派生类。

# 特性实例

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,AllowMultiple =true)]
class MyAttribute : Attribute
{
    public MyAttribute(int times)
    {
        Times = times;
    }
    public int Times { get;private set; }
    public string Name { get; set; }
}

//[MyAttribute(10)]
[MyAttribute(10,Name ="li"),MyAttribute(100,Name ="wang"),DebuggerStepThrough]
class MyClass
{
    public void Greeting()
    {
        Console.WriteLine("hello world!");
    }

    public void Debug()
    {

    }
}
class Program
{
    static void Main(string[] args)
    {
        Type classType = typeof(MyClass);
        //object[] getAttributes = classType.GetCustomAttributes(true);
        object[] getAttributes = classType.GetCustomAttributes(typeof(MyAttribute),true);
        foreach(object one in getAttributes)
        {
            Console.WriteLine($"Attribute :{one}");
            MyAttribute toMyDefineOne = one as MyAttribute;
            if(toMyDefineOne != null)
            {
                Console.WriteLine($"this attribute have times:{toMyDefineOne.Times},have Name:{toMyDefineOne.Name}");
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 初始化器

对象初始化器可以合并对象的实例化和初始化,集合初始化器则可以快速创建和填充集合。看实例:

public class Person{
    public string Name { get; set; }
    public int Age { get; set; }

    //public Person(string name,int age)
    //{
    //    Name = name;
    //    Age = age;
    //}

}

class Program
{
    static void Main(string[] args)
    {
        Person p1 = new Person
        {
            Name = "li",
            Age = 12
        };

        List<Person> persons = new List<Person>
        {
            new Person
            {
                Name="zhang",
                Age =20
            },
            new Person
            {
                Name = "li",
                Age =25
            }
        };
        Console.WriteLine(persons[1].Name);

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

初始化器比带参构造函数好的是更灵活,比如有的参数不想初始化,或者有无参初始化对象时需要提供默认构造函数。

# 类型推理

C#是一种强类型化的语言,每个变量都有固定的类型,只能接受该类型的值,C# 3引入了关键字var,它可以代替声明变量时的类型,但var不是一个具体的类型,也不是一个类型可变的类型,它只是依赖于编译器来确定变量的类型,使用这种方式来声明变量时,必须同时初始化该变量,因为如果没有初始值,编译器就无法确定变量的类型。比如下面:

var myArray = new int?[]{1,null,2};
1

# 匿名类型

在程序中有些类可能只是用来存储一组数据,这样的类只提供属性,在数据库或电子表格中,这样的类可看成是表的一行,这时可以使用匿名类型(anonymous type),匿名类型的属性是只读属性,如果要修改存储属性的值,则不能使用匿名类型,请看实例:

static void Main(string[] args)
{
    var persons = new[]
    {
        new {Name ="li",Age =42},
        new {Name ="wang",Age=34},
        new {Name = "li",Age =42},
    };

    Console.WriteLine(persons[0].GetHashCode());
    Console.WriteLine(persons[1].GetHashCode());
    Console.WriteLine(persons[2].GetHashCode());
    Console.WriteLine($"person[0] == person[1]: {persons[0] == persons[1]}");
    Console.WriteLine($"person[0] == person[2]: {persons[0] == persons[2]}");
    Console.WriteLine($"person[0] equals person[2]: {persons[0].Equals(persons[2])}");

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注意使用==比较就不会像Equals那样得到True的结果,它比较的是对象的引用。它与对象初始化器比较有两点不同:

  1. 使用了var关键字,匿名类型没有可以使用的标识符,它在内部有一个标识符,但不能在代码中使用,通过visual studio的提示信息可以看到匿名类型是'a类型。
  2. 在new关键字后面没有指定类型名。

# 动态查找

上面提到var并不是一个类型,从C# 4后引入了"动态变量"的概念,动态变量是类型可变的变量,它有以下应用场景:

  1. 希望使用C#处理另一种语言创建的对象,包括与旧技术的操作,比如Component Object Model(COM),也包括处理动态语言,比如JavaScript,Python和Ruby等。
  2. 希望处理未知的C#对象,比如需要一些泛型代码来处理接收的各种输入。

在后台,动态查找功能由Dynamic Language Runtime(动态语言运行库,DLR)支持,与CLR一样,DLR是 .Net 4.7的一部分。动态类型仅存在于编译期间,在运行期间它会被System.Object类型替代。

来看下面实例:

class MyClass1
{
  public int Add(int var1, int var2) => var1 + var2;
}

class MyClass2
{

}
class Program
{
  static int callCount = 0;
  static dynamic GetValue()
  {
      if(callCount++ == 0)
      {
          return new MyClass1();
      }
      return new MyClass2();
  }
  static void Main(string[] args)
  {
      dynamic class1 = GetValue();
      dynamic class2 = GetValue();
      Console.WriteLine($"class1 : {class1.ToString()}");
      Console.WriteLine($"class2 : {class2.ToString()}");
      Console.WriteLine($"class1 add :{class1.Add(1, 2)}");
      //Console.WriteLine($"class2 add :{class2.Add(1, 2)}");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 高级方法参数

C# 4扩展了定义和使用方法参数的方式,这主要是为了响应使用外部接口时出现的一个特殊问题,比如Microsoft Office编程模型,它的一些方法中有大量的参数,许多参数并不是每次调用需要的,这就导致调用一个方法时,实参中会有很多null,当然我们可以让这个方法有多个重载版本,但这要维护更多的代码,增加了代码的复杂度。为了解决这个问题,C# 4增加了可选参数和命名参数。

# 可选参数

可选参数是在方法定义中为参数提供一个默认值,使其成为可选参数,如果调用该方法时没有为该参数提供实参,则使用默认值即可。需要注意以下几点:

  • 在定义方法时为形参提供默认值,默认值必须是字面值,常量值或者默认值类型值。
  • 在定义方法时可对形参使用Optional关键字,此特性定义在System.Runtime.InteropServices名称空间中,如果使用此种语法,则不能为形参提供默认值。
  • 可选参数必须放在方法参数列表的末尾,没有默认值的参数不能放在有默认值的参数后面。

# 命名参数

使用可选参数时,发现某个方法有几个可选参数,但只想给其中第三个可选参数传递值,这就需要给前两个可选参数提供值,命名参数(named parameters)允许指定要使用哪个参数,这不需要在方法定义中进行任何特殊处理,它是一种在调用方法时使用的技术。注意:

  • 只要命名参数存在,就可以采用这种方式指定需要的任意多个参数,而且参数的顺序是任意的。
  • 可以仅给方法调用中的某些可选参数使用命名参数,当方法签名中有多个可选参数和必选参数时,可以首先指定必选参数,再使用命名参数指定可选参数。

# 参数实例

static void TestParameter(string name,int age=20,string degree="master")
{
    Console.WriteLine($"name:{name},age:{age},degree:{degree}");
}
static void TestParam(string name,[Optional] string marriage,[Optional] int age)
{
    Console.WriteLine($"name:{name},marraige:{marriage},age:{age}");
}
static void Main(string[] args)
{
    TestParameter("xie", 17);
    TestParam("li", age: 20);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# Lambda表达式

Lambda表达式是一种结构,在与LINQ结合或匿名方法结合时很有用。上面事件的匿名函数用过Lambda表达式,现在主要以匿名方法为主介绍Lambda表达式。

Lambda表达式有3部分组成:

  • 放在括号中的参数列表
  • =>运算符
  • Lambda语句体

注意:

  • Lambda表达式可以使用类型推理来确定传递的参数类型,当然也可以使用显式的参数类型,即在参数前面带着类型,注意它的参数列表要不都是显式类型的,要不都是隐式类型的,不能既有显式类型又有隐式类型,如果只有一个隐式类型化的参数就可以省略括号,也可以没有参数只有括号,这意味着它不需要参数。
  • Lambda表达式如果只有一条语句,就可以直接写就行,返回值返回这条语句的值,如果有多条语句就需要在=>后面加上大括号,并且像方法一样使用return来返回值。
  • Lambda表达式比匿名函数的优点是灵活,比如隐式类型化的参数,但它的缺点就是不能重用,如果多个语句需要这个Lambda表达式时最好定义一个非匿名的方法来替代。

看实例:

delegate int TwoIntOper(int paramA, int paramB);

class Program
{
    static void TwoIntFunc(int paramA,int paramB,TwoIntOper oper)
    {
        Console.WriteLine(oper(paramA,paramB));
    }
    static void Main(string[] args)
    {
        TwoIntFunc(2, 4, (int a, int b) => a + b);
        TwoIntFunc(2, 4, (a, b) => a + b);
        TwoIntFunc(2, 4, (a, b) => {Console.WriteLine("this is your function"); return a - b; });
        TwoIntFunc(2, 4, delegate(int a, int b) {
            return a - b;
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 表达式树

可以使用两种方式来理解Lambda表达式:

  1. Lambda表达式是一个委托,可以把它赋值给一个委托类型,可以用下面的委托来表示Lambda表达式:
    • Action:Lambda表达式不带参数,返回类型是void。
    • Action<>:Lambda表达式最多有8个参数,返回类型是void。
    • Func<>:Lambda表达式最多有8个参数,返回类型是它的最后一个参数,也就是Func可以有9个参数。
  2. 可以把Lambda表达式解释为表达式树,表达式树是Lambda表达式的抽象表示,不能直接执行。可使用表达式树以编程的方式来分析Lambda表达式,执行操作,来响应Lambda表达式。

理解上面对后面的LINQ至关重要,LINQ框架包含一个泛型类Expression<>,可用于封装Lambda表达式,这个类可提取用C#编写的Lambda表达式,把它转换成相应的SQL脚本以便在数据库中直接执行。

来看实例:

static void Main(string[] args)
{
   string[] fruits = { "Apple", "Orange", "Banana" };
   Console.WriteLine(fruits.Aggregate((a,b)=>a+" "+b));
   Console.WriteLine(fruits.Aggregate<string,int>(0,(a,b)=>a+b.Length));
   Console.WriteLine(fruits.Aggregate<string,string,string>("fruits:",(a,b)=>a+" "+b,a=>a));
   Console.WriteLine(fruits.Aggregate<string,string,int>("fruits:",(a,b)=>a+" "+b,a=>a.Length));
}
1
2
3
4
5
6
7
8