C# 进阶笔记

C#(发音为 C sharp,正确写法应为 C♯),是一种简单、现代、通用、面向对象的编程语言,支持跨平台。支持结构化、面向对象、泛型等多种编程范型。深受 Visual Basic、Java、C/C++ 的影响。在很多方面与 Java 相似,但在面向对象特性上拥有许多优势。

前言

本文不包含 C# 语法中比较基础的部分,仅包含较为高级的语法。

可空类型

C# 提供了一个特殊的数据类型——nullable 类型(可空类型),可空类型可以表示其基础值类型正常范围内的值,再加上一个 null 值。

在处理数据库和其他包含可能未赋值的元素的数据类型时,将 null 赋值给数值类型或布尔型的功能特别有用。例如,数据库中的布尔型字段可以存储值 true 或 false,或者,该字段也可以未定义。

声明一个可空类型的语法如下:

data-type? variable-name = null;

下面的示例演示了可空数据类型的用法:

using System;

namespace 可空类型
{
    class NullablesAtShow
    {
        static void Main(string[] args)
        {
            int? num1 = null;
            int? num2 = 45;
            double? num3 = new double?();
            double? num4 = 3.14157;

            bool? boolval = new bool?();

            // 显示值
            Console.WriteLine("显示可空类型的值:" + num1 + ", " + num2 + ", " + num3 + ", " + num4);
            Console.WriteLine("一个可空的布尔值:" + boolval);
        }
    }
}

结果如下:

显示可空类型的值:, 45, , 3.14157
一个可空的布尔值:

Null 合并运算符

Null 合并运算符用于定义可空类型和引用类型的默认值。Null 合并运算符为类型转换定义了一个预设值,以防可空类型的值为 Null。Null 合并运算符把操作数类型隐式转换为另一个可空(或不可空)的值类型的操作数的类型。

如果第一个操作数的值为 null,则运算符返回第二个操作数的值,否则返回第一个操作数的值。

下面的示例演示了这点:

using System;

namespace 可空类型
{
    class NullablesAtShow
    {
        static void Main(string[] args)
        {
            double? num1 = null;
            double? num2 = 3.14157;
            double num3;
            num3 = num1 ?? 5.34;
            Console.WriteLine("num3 的值:" + num3);
            num3 = num2 ?? 5.34;
            Console.WriteLine("num3 的值:" + num3);
        }
    }
}

结果如下:

num3 的值:5.34
num3 的值:3.14157

数组

数组的初始化示例:

double[] balance = new double[10];

赋值给数组有多种方式:

  • 在声明数组的同时给数组赋值,如:

    double[] balance = { 2340.0, 4523.69, 3421.0 };
    
  • 创建并初始化一个数组,如:

    int[] marks = new int[5] { 99, 98, 92, 97, 95 };
    
  • 在上一种情况下,可以省略数组的大小,如:

    int[] marks = new int[] { 99, 98, 92, 97, 95 };
    
  • 赋一个数组给另一个目标数组。在这种情况下,目标和源会指向相同的内存位置:

    int[] marks = new int[] { 99, 98, 92, 97, 95 };
    int[] score = marks;
    

多维数组

三维数组声明示例:

int[,,] m;

多维数组可以通过在括号内为每行指定值来进行初始化。下面是一个带有 3 行 4 列的数组:

int[,] a = new int[3, 4]  {
    { 0, 1, 2, 3} ,   // 初始化第 0 行
    { 4, 5, 6, 7} ,   // 初始化第 1 行
    { 8, 9, 10, 11}   // 初始化第 2 行
};

同样地,在上述种情况下,可以省略数组的大小:

int[,] a = new int[,]  {
    { 0, 1, 2, 3} ,   // 初始化第 0 行
    { 4, 5, 6, 7} ,   // 初始化第 1 行
    { 8, 9, 10, 11}   // 初始化第 2 行
};

交错数组

交错数组是数组的数组。

可以声明一个带有 int 值的交错数组 scores,如下所示:

int[][] scores;

创建该数组:

scores = new int[5][]; 
for (int i = 0; i < scores.Length; i++)
{
    scores[i] = new int[4];
}

由于交错数组的每一行都是一个数组,所以创建时必须指定行数,即指定所包含的一维数组的个数。

可以初始化一个交错数组,如下所示:

scores = new int[2][]
{
    new int[] { 92, 93, 94 },
    new int[] { 85, 66, 87, 88 }
};

同样地,也可以省略数组的大小:

scores = new int[][]
{
    new int[] { 92, 93, 94 },
    new int[] { 85, 66, 87, 88 }
};

传递数组给函数

将数组作为函数的参数时,使用不带索引的数组名称即可。

下面的示例演示了如何传递数组给函数:

using System;

namespace 传递数组给函数
{
    class Program
    {
        static double getAverage(int[] arr, int size)
        {
            int i;
            double avg;
            int sum = 0;

            for (i = 0; i < size; ++i)
            {
                sum += arr[i];
            }

            avg = (double)sum / size;
            return avg;
        }

        static void Main(string[] args)
        {
            /* 一个带有 5 个元素的 int 数组 */
            int[] balance = new int[] { 1000, 2, 3, 17, 50 };

            /* 传递数组的指针作为参数 */
            double avg = getAverage(balance, balance.Length);

            /* 输出返回值 */
            Console.WriteLine("平均值是:" + avg);
        }
    }
}

结果如下:

平均值是:214.4

参数数组

有时,当声明一个方法时,不能确定要传递给函数作为参数的参数数目。C# 参数数组解决了这个问题,参数数组通常用于传递未知数量的参数给函数。

在使用数组作为形参时,C# 提供了 params 关键字,使调用数组为形参的方法时,既可以传递数组实参,也可以只传递一组数组。params 的使用格式为:

public 返回类型 方法名(params 类型名[] 数组名)

下面的示例演示了如何使用参数数组:

using System;

namespace 参数数组
{
    class Program
    {
        static int AddElements(params int[] arr)
        {
            int sum = 0;
            foreach (int i in arr)
            {
                sum += i;
            }
            return sum;
        }

        static void Main(string[] args)
        {
            int sum = AddElements(512, 720, 250, 567, 889);
            Console.WriteLine("总和是:" + sum);
        }
    }
}

结果如下:

总和是:2938

多态性

静态多态性

在编译时,函数和对象的连接机制被称为早期绑定,也被称为静态绑定。

C# 提供了两种技术来实现静态多态性:

  • 函数重载
  • 运算符重载

动态多态性

C# 允许使用关键字 abstract 创建抽象类,用于提供接口的部分类的实现。当一个派生类继承自抽象类时,实现即完成。

抽象类包含抽象方法,抽象方法可被派生类实现。派生类具有更专业的功能。

下面是有关抽象类的一些规则:

  • 抽象类不可创建实例,只能被继承;
  • 不能在抽象类外部声明抽象方法。

下面的示例演示了一个抽象类:

using System;

namespace 抽象类
{
    class Program
    {
        static void Main(string[] args)
        {
            Rectangle r = new Rectangle(10, 7);
            double a = r.area();
            Console.WriteLine("面积:" + a);
        }
    }
}
using System;

namespace 抽象类
{
    public class Rectangle : Shape
    {
        private int length;
        private int width;

        public Rectangle(int len = 0, int wid = 0)
        {
            length = len;
            width = wid;
        }

        public override int area()
        {
            Console.Write("Rectangle 类的");
            return length * width;
        }
    }
}
namespace 抽象类
{
    public abstract class Shape
    {
        public abstract int area();
    }
}

分开的代码块表示不同的类,各存于一个文件。

结果如下:

Rectangle 类的面积:70

当有一个定义在类中的函数需要在继承类中实现时,可以使用虚方法。虚方法是使用关键字 virtual 声明的。虚方法可以在不同的继承类中有不同的实现。对虚方法的调用是在运行时发生的。

动态多态性是通过抽象类虚方法实现的。

下面的程序演示了这点:

using System;

namespace 虚方法
{
    class Program
    {
        static void Main(string[] args)
        {
            Caller c = new Caller();
            Rectangle r = new Rectangle(10, 7);
            Triangle t = new Triangle(10, 5);
            c.CallArea(r);
            c.CallArea(t);
        }
    }
}
using System;

namespace 虚方法
{
    public class Caller
    {
        public void CallArea(Shape sh)
        {
            int a = sh.area();
            Console.WriteLine("面积:" + a);
        }
    }
}
using System;

namespace 虚方法
{
    public class Rectangle : Shape
    {
        public Rectangle(int wid = 0, int hei = 0) : base(wid, hei)
        {
        }

        public override int area()
        {
            Console.Write("Rectangle 类的");
            return width * height;
        }
    }
}
using System;

namespace 虚方法
{
    public class Triangle : Shape
    {
        public Triangle(int wid = 0, int hei = 0) : base(wid, hei)
        {
        }

        public override int area()
        {
            Console.Write("Triangle 类的");
            return width * height / 2;
        }
    }
}
using System;

namespace 虚方法
{
    public class Shape
    {
        protected int width, height;

        public Shape(int wid = 0, int hei = 0)
        {
            width = wid;
            height = hei;
        }

        public virtual int area()
        {
            Console.Write("基类的");
            return 0;
        }
    }
}

结果如下:

Rectangle 类的面积:70
Triangle 类的面积:25

可以注意到,以下两种情况,都需要加上 override 关键字:

  • 派生类实现抽象基类的抽象方法;
  • 派生类实现基类的虚方法。

属性

抽象属性

抽象类可拥有抽象属性(abstract property),这些属性应在派生类中被实现。下面的示例说明了这点:

using System;

namespace 抽象属性
{
    class Program
    {
        static void Main(string[] args)
        {
            // 创建一个新的 Student 对象
            Student s = new Student();

            // 设置 student 的 code、name 和 age
            s.Code = "001";
            s.Name = "Zara";
            s.Age = 9;
            Console.WriteLine("Student Info: " + s);

            s.Age += 1; // 增加年龄
            Console.WriteLine("Student Info: " + s);
        }
    }
}
using System;

namespace 抽象属性
{
    public class Student : Person
    {
        public string Code { get; set; } = "N.A";
        public override string Name { get; set; } = "N.A";
        public override int Age { get; set; } = 0;

        public override string ToString()
        {
            return "Code = " + Code + ", Name = " + Name + ", Age = " + Age;
        }
    }
}
using System;

namespace 抽象属性
{
    public abstract class Person
    {
        public abstract string Name { get; set; }
        public abstract int Age { get; set; }
    }
}

结果如下:

Student Info: Code = 001, Name = Zara, Age = 9
Student Info: Code = 001, Name = Zara, Age = 10

索引器

索引器(indexer) 允许一个对象可以像数组一样被索引。当为类定义一个索引器时,该类的行为就会像一个虚拟数组一样。可以使用数组访问运算符([ ])来访问该类的实例。

一维索引器的语法如下:

element-type this[int index] 
{
   // get 访问器
   get 
   {
      // 返回 index 指定的值
   }

   // set 访问器
   set 
   {
      // 设置 index 指定的值 
   }
}

索引器的行为的声明在某种程度上类似于属性。就像属性一样,可以使用 getset 访问器来定义索引器。但是,属性返回或设置一个特定的数据成员,而索引器返回或设置对象实例的一个特定值。换句话说,它把实例数据分为更小的部分,并索引每个部分,获取或设置每个部分。

定义一个属性包括提供属性名称。索引器定义的时候不带有名称,但带有 this 关键字,它指向对象实例。下面的示例演示了这个概念:

using System;

namespace 索引器
{
    class Program
    {
        static void Main(string[] args)
        {
            IndexedNames names = new IndexedNames();
            names[0] = "Zara";
            names[1] = "Riz";
            names[2] = "Nuha";
            names[3] = "Asif";
            names[4] = "Davinder";
            names[5] = "Sunil";
            names[6] = "Rubic";

            for (int i = 0; i < IndexedNames.size; i++)
            {
                Console.WriteLine(names[i]);
            }
        }
    }
}
using System;

namespace 索引器
{
    public class IndexedNames
    {
        private string[] namelist;
        public static int size = 10;

        public IndexedNames()
        {
            namelist = new string[size];

            for (int i = 0; i < size; i++)
            {
                namelist[i] = "N. A.";
            }
        }

        public string this[int index]
        {
            get
            {
                string tmp;

                // 数组未越界
                if (index >= 0 && index <= size - 1)
                {
                    tmp = namelist[index];
                }
                else
                {
                    tmp = string.Empty;
                }
                
                return tmp;
            }

            set
            {
                if (index >= 0 && index <= size - 1)
                {
                    namelist[index] = value;
                }
            }
        }
    }
}

结果如下:

Zara
Riz
Nuha
Asif
Davinder
Sunil
Rubic
N. A.
N. A.
N. A.

重载索引器

索引器可被重载。索引器声明的时候也可带有多个参数,且每个参数可以是不同的类型。没有必要让索引器必须是整型的。索引器可以是其他类型,例如字符串类型。

using System;

namespace 重载索引器
{
    class Program
    {
        static void Main(string[] args)
        {
            IndexedNames names = new IndexedNames();
            names[0] = "Zara";
            names[1] = "Riz";
            names[2] = "Nuha";
            names[3] = "Asif";
            names[4] = "Davinder";
            names[5] = "Sunil";
            names[6] = "Rubic";

            // 使用带有 int 参数的第一个索引器
            for (int i = 0; i < IndexedNames.size; i++)
            {
                Console.WriteLine(names[i]);
            }

            // 使用带有 string 参数的第二个索引器
            Console.WriteLine(names["Nuha"]);
        }
    }
}
using System;

namespace 重载索引器
{
   public class IndexedNames
   {
       private string[] namelist;
       public static int size = 10;

       public IndexedNames()
       {
           namelist = new string[size];

           for (int i = 0; i < size; i++)
           {
               namelist[i] = "N. A.";
           }
       }

       public string this[int index]
       {
           get
           {
               string tmp;

               // 数组未越界
               if (index >= 0 && index <= size - 1)
               {
                   tmp = namelist[index];
               }
               else
               {
                   tmp = string.Empty;
               }

               return tmp;
           }

           set
           {
               if (index >= 0 && index <= size - 1)
               {
                   namelist[index] = value;
               }
           }
       }

       public int this[string name]
       {
           get
           {
               int index;

               for (index = 0; index < size; index++)
               {
                   if (namelist[index] == name)
                   {
                       return index;
                   }
               }

               return index;
           }
       }
   }
}

结果如下:

Zara
Riz
Nuha
Asif
Davinder
Sunil
Rubic
N. A.
N. A.
N. A.
2

委托

C# 中的委托类似于 C/C++ 中函数的指针。

委托(delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时改变。

委托特别用于实现事件和回调方法。所有的委托都派生自 System.Delegate 类。

声明委托

委托声明决定了可由该委托引用的方法。委托可指向一个与其具有相同签名的方法。

例如,假设有一个委托:

public delegate int MyDelegate(string s);

上面的委托可用于引用任何一个带有一个单一的 string 参数的方法,并返回一个 int 类型变量。

声明委托的语法如下:

delegate <return-type> <delegate-name>(<parameter-list>)

实例化委托

一旦声明了委托类型,委托对象必须使用 new 关键字来创建,且与一个特定的方法有关。当创建委托时,传递到 new 语句的参数就像方法调用一样书写,但是不带有参数。例如:

public delegate void printString(string s);
// …
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);

下面的示例演示了委托的声明、实例化和使用,该委托可用于引用带有一个整型参数的方法,并返回一个整型值。

using System;

delegate int NumberChanger(int n);
namespace 委托
{
    class Program
    {
        static void Main(string[] args)
        {
            TestDelegate d = new TestDelegate();

            // 创建委托实例
            NumberChanger ncAdd = new NumberChanger(d.AddNum);
            NumberChanger ncMult = new NumberChanger(d.MultNum);

            // 使用委托对象调用方法
            ncAdd(25);
            Console.WriteLine("Value of Num: " + d.Num);
            ncMult(5);
            Console.WriteLine("Value of Num: " + d.Num);
        }
    }
}
using System;

namespace 委托
{
   public class TestDelegate
   {
       public int Num { get; set; } = 10;

       public int AddNum(int p)
       {
           Num += p;
           return Num;
       }

       public int MultNum(int q)
       {
           Num *= q;
           return Num;
       }
   }
}

结果如下:

Value of Num: 35
Value of Num: 175

委托的多播

委托对象可使用 + 运算符进行合并。一个合并委托调用它所合并的两个委托。只有相同类型的委托可以合并。- 运算符可用于从合并的委托中移除组件委托。

使用这个特点,可以创建一个委托被调用时要调用的方法的调用列表。这称为委托的多播(multicasting),也叫组播。下面的示例演示了委托的多播:

using System;

delegate int NumberChanger(int n);
namespace 委托
{
    class Program
    {
        static void Main(string[] args)
        {
            TestDelegate d = new TestDelegate();

            // 创建委托实例
            NumberChanger nc;
            NumberChanger ncAdd = new NumberChanger(d.AddNum);
            NumberChanger ncMult = new NumberChanger(d.MultNum);

            nc = ncAdd;
            nc += ncMult;

            nc(5); // 调用多播
            Console.WriteLine("Value of Num: " + d.Num);
        }
    }
}
using System;

namespace 委托
{
    public class TestDelegate
    {
        public int Num { get; set; } = 10;

        public int AddNum(int p)
        {
            Num += p;
            return Num;
        }

        public int MultNum(int q)
        {
            Num *= q;
            return Num;
        }
    }
}

结果如下:

Value of Num: 75

委托的用途

下面的示例演示了委托的用法。委托 printString 可用于引用带有一个字符串作为输入的方法,并不返回任何东西。

使用这个委托来调用两个方法,第一个把字符串打印到控制台,第二个把字符串打印到文件:

using System;

namespace 委托
{
    class Program
    {
        static void Main(string[] args)
        {
            Print print = new Print();
            printString psScreen = new printString(print.WriteToScreen);
            printString psFile = new printString(print.WriteToFile);
            print.sendString(psScreen);
            print.sendString(psFile);
        }
    }
}
using System;
using System.IO;

// 委托声明
public delegate void printString(string s);
namespace 委托
{
    public class Print
    {
        private FileStream fs;
        private StreamWriter sw;

        // 打印到控制台
        public void WriteToScreen(string str)
        {
            Console.WriteLine("The String is: " + str);
        }
        
        // 打印到文件
        public void WriteToFile(string s)
        {
            fs = new FileStream(".\\message.txt", FileMode.Append, FileAccess.Write);
            sw = new StreamWriter(fs);
            sw.WriteLine(s);
            sw.Flush();
            sw.Close();
            fs.Close();
        }

        // 把委托作为参数,并使用它调用方法
        public void sendString(printString ps)
        {
            ps("Hello World");
        }
    }
}

结果如下:

$ dotnet run
The String is: Hello World

$ ls .
bin  message.txt  obj  Print.cs  Program.cs  TestDelegate.cs  委托.csproj
# 确实创建了 message.txt 文件

$ cat message.txt
Hello World
# message.txt 文件的内容也正确

匿名方法

匿名方法(anonymous method)提供了一种传递代码块作为委托参数的技术。匿名方法是没有名称只有主体的方法。

在匿名方法中不需要指定返回类型,它是从方法主体内的 return 语句推断的。

匿名方法是通过创建委托实例来声明的。例如:

delegate void NumberChanger(int n);
// …
NumberChanger nc = delegate(int x)
{
    Console.WriteLine("Anonymous Method: " + x);
};

代码块 Console.WriteLine("Anonymous Method: " + x); 是匿名方法的主体。

委托可以通过匿名方法调用,也可以通过命名方法调用,即,通过向委托对象传递方法参数。例如:

nc(10);

下面的示例演示了匿名方法的概念:

using System;

namespace 匿名方法
{
    class Program
    {
        static void Main(string[] args)
        {
            TestDelegate d = new TestDelegate();

            // 使用匿名方法创建委托实例
            NumberChanger ncAno = delegate (int x)
            {
                Console.WriteLine("Anonymous Method: " + x);
            };

            // 使用匿名方法调用委托
            ncAno(10);

            // 使用命名方法实例化委托
            NumberChanger ncNamed1 = new NumberChanger(d.AddNum);

            // 使用命名方法调用委托
            ncNamed1(5);

            // 使用另一个命名方法实例化委托
            NumberChanger ncNamed2 = new NumberChanger(d.MultNum);

            // 使用命名方法调用委托
            ncNamed2(2);
        }
    }
}
using System;

public delegate void NumberChanger(int n);
namespace 匿名方法
{
    public class TestDelegate
    {
        public int Num { get; set; } = 10;

        public void AddNum(int p)
        {
            Num += p;
            Console.WriteLine("Named Method: " + Num);
        }

        public void MultNum(int q)
        {
            Num *= q;
            Console.WriteLine("Named Method: " + Num);
        }
    }
}

结果如下:

Anonymous Method: 10
Named Method: 15
Named Method: 30

事件

事件(event)基本上是一个用户操作,如按键、点击、鼠标移动等等;或者是一些出现,如系统生成的通知。应用程序需要在事件发生时响应事件,例如中断。事件用于进程间通信。

通过事件使用委托

事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。

  • 包含事件的类用于发布事件,称为发布器(publisher)类。
  • 其他接受该事件的类称为订阅器(subscriber)类。

事件使用发布-订阅(publisher-subscriber)模型。

  • 发布器是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器类的对象调用这个事件,并通知其他的对象。
  • 订阅器是一个接受事件并提供事件处理程序的对象。在发布器类中的委托调用订阅器类中的方法(事件处理程序)。

声明事件

在类的内部声明事件,首先必须声明该事件的委托类型。例如:

public delegate void BoilerLogHandler(string status);

然后声明事件本身,使用 event 关键字:

// 基于上面的委托定义事件
public event BoilerLogHandler BoilerEventLog;

上面的代码定义了一个名为 BoilerLogHandler 的委托和一个名为 BoilerEventLog 的事件,该事件在生成的时候会调用委托。

示例 1

using System;

namespace 事件
{
    class Program
    {
        static void Main(string[] args)
        {
            EventTest e = new EventTest(5);
            e.SetNum(7);
            e.SetNum(11);
        }
    }
}
using System;

namespace 事件
{
    public class EventTest
    {
        private int Num;
        private delegate void NumManipulationHandler();
        private event NumManipulationHandler ChangeNum;

        private void OnNumChanged()
        {
            if (ChangeNum != null)
            {
                ChangeNum();
            }
            else
            {
                Console.WriteLine("Event fired!");
            }
        }

        public EventTest(int n)
        {
            SetNum(n);
        }

        public void SetNum(int n)
        {
            if (Num != n)
            {
                Num = n;
                OnNumChanged();
            }
        }
    }
}

结果如下:

Event fired!
Event fired!
Event fired!

示例 2

using System;

namespace 事件
{
    // 事件订阅器
    class RecordBoilerInfo
    {
        static void Logger(string info)
        {
            Console.WriteLine(info);
        }

        static void Main(string[] args)
        {
            BoilerInfoLogger filelog = new BoilerInfoLogger(".\\boiler.txt");
            DelegateBoilerEvent boilerEvent = new DelegateBoilerEvent();
            boilerEvent.BoilerEventLog += new DelegateBoilerEvent.BoilerLogHandler(Logger);
            boilerEvent.BoilerEventLog += new DelegateBoilerEvent.BoilerLogHandler(filelog.Logger);
            boilerEvent.LogProcess();
            filelog.Close();
        }
    }
}
using System;
using System.IO;

namespace 事件
{
    // 保留写入日志文件的条款
    public class BoilerInfoLogger
    {
        private FileStream fs;
        private StreamWriter sw;

        public BoilerInfoLogger(string filename)
        {
            fs = new FileStream(filename, FileMode.Append, FileAccess.Write);
            sw = new StreamWriter(fs);
        }

        public void Logger(string info)
        {
            sw.WriteLine(info);
        }

        public void Close()
        {
            sw.Close();
            fs.Close();
        }
    }
}
using System;

namespace 事件
{
    // 事件发布器
    public class DelegateBoilerEvent
    {
        public delegate void BoilerLogHandler(string status);

        // 基于上面的委托定义事件
        public event BoilerLogHandler BoilerEventLog;

        public void LogProcess()
        {
            string remarks = "O. K";
            Boiler b = new Boiler(100, 12);
            int t = b.Temp;
            int p = b.Pressure;
            if (t > 150 || t < 80 || p < 12 || p > 15)
            {
                remarks = "Need Maintenance";
            }
            OnBoilerEventLog("Logging Info:" + Environment.NewLine);
            OnBoilerEventLog("Temparature: " + t + Environment.NewLine + "Pressure: " + p + Environment.NewLine);
            OnBoilerEventLog("Message: " + remarks);
        }

        private void OnBoilerEventLog(string message)
        {
            if (BoilerEventLog != null)
            {
                BoilerEventLog(message);
            }
        }
    }
}
using System;

namespace 事件
{
    public class Boiler
    {
        public int Temp { get; set; }
        public int Pressure { get; set; }

        public Boiler(int temp, int pressure)
        {
            Temp = temp;
            Pressure = pressure;
        }
    }
}

结果如下:

Logging Info:

Temparature: 100
Pressure: 12

Message: O. K

特性

特性(attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。可以通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。

特性用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。

规定特性

规定特性的语法如下:

[attribute(positional_parameters, name_parameter = value, …)]
element

特性的名称和值是在方括号内规定的,放置在它所应用的元素之前。positional_parameters 规定必需的信息,name_parameter 规定可选的信息。

预定义特性

.NET 提供了以下几种特性:

  • AttributeUsage
  • Conditional
  • Obsolete

AttributeUsage

预定义特性 AttributeUsage 描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。

规定该特性的语法如下:

[AttributeUsage(
    validon,
    AllowMultiple = allowmultiple,
    Inherited = inherited
)]

其中,

  • validon 规定特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是AttributeTargets.All
  • allowmultiple(可选)为该特性的 AllowMultiple 属性提供一个布尔值。如果为 true,则该特性是多用的。默认值是 false,即单用的。
  • 参数 inherited(可选)为该特性的 Inherited 属性提供一个布尔值。如果为 true,则该特性可被派生类继承。默认值是 false,不可继承。

例如:

[AttributeUsage(
    AttributeTargets.Class |
    AttributeTargets.Constructor |
    AttributeTargets.Field |
    AttributeTargets.Method |
    AttributeTargets.Property,
    AllowMultiple = true
)]

Conditional

这个预定义特性标记了一个条件方法,其执行依赖于它顶的预处理标识符。

它会引起方法调用的条件编译,取决于指定的值,比如 DebugTrace。例如,当调试代码时显示变量的值。

规定该特性的语法如下:

[Conditional(
    conditionalSymbol
)]

例如:

[Conditional("DEBUG")]

下面的示例演示了该特性:

using System;

namespace 特性
{
    class Program
    {
        static void func1()
        {
            MyClass.Message("In function 1.");
            func2();
        }

        static void func2()
        {
            MyClass.Message("In function 2.");
        }
        
        static void Main(string[] args)
        {
            MyClass.Message("In Main function.");
            func1();
        }
    }
}
#define DEBUG
using System;
using System.Diagnostics;

namespace 特性
{
    public class MyClass
    {
        [Conditional("DEBUG")]
        public static void Message(string msg)
        {
            Console.WriteLine(msg);
        }
    }
}

结果如下:

In Main function.
In function 1.
In function 2.

Obsolete

这个预定义特性标记了不应被使用的程序实体。它可以通知编译器丢弃某个特定的目标元素。例如,当一个新方法被用在一个类中,但是仍然想要保持类中的旧方法,可以通过显示一个应该使用新方法而不是旧方法的消息,来把它标记为 obsolete(过时的)。

规定该特性的语法如下:

[Obsolete(
    message
)]
[Obsolete(
    message,
    iserror
)]

其中,

  • message 是一个字符串,描述项目过时的原因以及该替代使用什么。
  • iserror 是一个布尔值。如果该值为 true,编译器应把该项目的使用当作一个错误。默认值是 false,编译器生成一个警告。

下面的示例演示了该特性:

using System;

namespace 特性
{
    class Program
    {
        static void Main(string[] args)
        {
            MyClass.OldMethod();
        }
    }
}
#define DEBUG
using System;

namespace 特性
{
    public class MyClass
    {
        [Obsolete("Don't use OldMethod, use NewMethod instead", true)]
        public static void OldMethod()
        {
            Console.WriteLine("This is the old method.");
        }

        public static void NewMethod()
        {
            Console.WriteLine("This is the new method.");
        }
    }
}

当尝试编译该程序时,编译器会给出一个错误消息说明:

Don't use OldMethod, use NewMethod instead

创建自定义特性

可以创建自定义特性,用于存储声明性的信息,且可在运行时被检索。该信息根据设计标准和应用程序需要,可与任何目标元素相关。

创建并使用自定义特性包含四个步骤:

  • 声明自定义特性;
  • 构建自定义特性;
  • 在目标程序元素上应用自定义特性;
  • 通过反射访问特性。

最后一个步骤包含编写一个简单的程序来读取元数据以便查找各种符号。元数据是用于描述其他数据的数据和信息。该程序应使用反射来在运行时访问特性。

声明自定义特性

一个新的自定义特性应派生自 System.Attribute 类。例如:

// 一个自定义特性 BugFix 被赋给类及其成员
[AttributeUsage(
    AttributeTargets.Class |
    AttributeTargets.Constructor |
    AttributeTargets.Field |
    AttributeTargets.Method |
    AttributeTargets.Property,
    AllowMultiple = true
)]
public class DebugInfo : System.Attribute

声明了一个名为 DebugInfo 的自定义特性。

构建自定义特性

接下来构建一个名为 DebugInfo 的自定义特性,该特性将存储调试程序获得的信息。它存储下面的信息:

  • bug 的代码编号
  • 辨认该 bug 的开发人员名字
  • 最后一次审查该代码的日期
  • 一个存储了开发人员标记的字符串消息

DebugInfo 类将带有三个用于存储前三个信息的私有属性和一个用于存储消息的公有属性。所以 bug 编号、开发人员名字和审查日期将是 DebugInfo 类的必需的定位(positional)参数,消息将是一个可选的命名(named)参数。

每个特性必须至少有一个构造函数。必需的定位参数应通过构造函数传递。下面的代码演示了 DebugInfo 类:

using System;

namespace 特性
{
    // 一个自定义特性 BugFix 被赋给类及其成员
    [AttributeUsage(
        AttributeTargets.Class |
        AttributeTargets.Constructor |
        AttributeTargets.Field |
        AttributeTargets.Method |
        AttributeTargets.Property,
        AllowMultiple = true
    )]
    public class DebugInfo : System.Attribute
    {
        public int BugNumber { get; }
        public string Developer { get; }
        public string LastReview { get; }
        public string Message { get; set; }

        public DebugInfo(int bugNumber, string developer, string lastView)
        {
            BugNumber = bugNumber;
            Developer = developer;
            LastReview = lastView;
        }
    }
}

应用自定义特性

通过把特性放置在紧接着它的目标之前,来应用该特性:

using System;

namespace 特性
{
    [DebugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")]
    [DebugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")]
    public class Rectangle
    {
        // 成员变量
        protected double length;
        protected double width;
        
        public Rectangle(double l, double w)
        {
            length = l;
            width = w;
        }

        [DebugInfo(55, "Zara Ali", "19/10/2012", Message = "Return type mismatch")]
        public double GetArea()
        {
            return length * width;
        }

        [DebugInfo(56, "Zara Ali", "19/10/2012")]
        public void Display()
        {
            Console.WriteLine("Length: " + length);
            Console.WriteLine("Width: " + width);
            Console.WriteLine("Area: " + GetArea());
        }
    }
}

反射

反射(reflection) 对象用于在运行时获取类型信息。该类位于 System.Reflection 命名空间中,可访问一个正在运行的程序的元数据,这个命名空间其包含了允许获取有关应用程序信息及向应用程序动态添加类型、值和对象的类。

反射有以下用途:

  • 允许在运行时查看属性的信息。
  • 允许审查集合中的各种类型,以及实例化这些类型。
  • 允许延迟绑定的方法和属性。
  • 允许在运行时创建新类型,然后使用这些类型执行一些任务。

前面提到过,使用反射可以查看属性的信息。

System.Reflection 类的 MemberInfo 对象需要初始化,用于发现与类相关的属性。为了做到这点,可以定义目标类的一个对象,如下:

System.Reflection.MemberInfo info = typeof(MyClass);

下面的程序演示了这点:

using System;

namespace 反射
{
    class Program
    {
        static void Main(string[] args)
        {
            System.Reflection.MemberInfo info = typeof(MyClass);
            var attributes = info.GetCustomAttributes(true);
            for (int i = 0; i < attributes.Length; i++)
            {
                Console.WriteLine(attributes[i]);
            }
        }
    }
}
namespace 反射
{
    [HelpAttribute("Information on the class MyClass")]
    public class MyClass
    {
    }
}
using System;

namespace 反射
{
    [AttributeUsage(AttributeTargets.All)]
    public class HelpAttribute : System.Attribute
    {
        public readonly string Url;
        public string Topic { get; set; } // 命名(named)参数

        public HelpAttribute(string url)
        {
            Url = url;
        }
    }
}

结果会显示附加到类 MyClass 上的自定义属性:

反射.HelpAttribute

下面这个示例将使用在特性章节中创建的 DebugInfo 属性,并使用反射来读取 Rectangle 类中的元数据。

using System;
using System.Reflection;

namespace 反射
{
    class Program
    {
        static void Main(string[] args)
        {
            Rectangle r = new Rectangle(4.5, 7.5);
            r.Display();
            var type = typeof(Rectangle);

            // 遍历 Rectangle 类的属性
            foreach (Object attributes in type.GetCustomAttributes(false))
            {
                DebugInfo dbi = (DebugInfo)attributes;
                if (dbi != null)
                {
                    Console.WriteLine("Bug Number: " + dbi.BugNumber);
                    Console.WriteLine("Developer: " + dbi.Developer);
                    Console.WriteLine("Last Reviewed: " + dbi.LastReview);
                    Console.WriteLine("Remarks: " + dbi.Message);
                }
            }

            // 遍历方法属性
            foreach (MethodInfo m in type.GetMethods())
            {
                foreach (Attribute a in m.GetCustomAttributes(true))
                {
                    DebugInfo dbi = (DebugInfo)a;
                    if (dbi != null)
                    {
                        Console.WriteLine("Bug Number: " + dbi.BugNumber + ", " + "for Method: " + m.Name);
                        Console.WriteLine("Developer: " + dbi.Developer);
                        Console.WriteLine("Last Reviewed: " + dbi.LastReview);
                        Console.WriteLine("Remarks: " + dbi.Message);
                    }
                }
            }
        }
    }
}
using System;

namespace 反射
{
    [DebugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")]
    [DebugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")]
    public class Rectangle
    {
        // 成员变量
        protected double length;
        protected double width;

        public Rectangle(double l, double w)
        {
            length = l;
            width = w;
        }

        [DebugInfo(55, "Zara Ali", "19/10/2012", Message = "Return type mismatch")]
        public double GetArea()
        {
            return length * width;
        }

        [DebugInfo(56, "Zara Ali", "19/10/2012")]
        public void Display()
        {
            Console.WriteLine("Length: " + length);
            Console.WriteLine("Width: " + width);
            Console.WriteLine("Area: " + GetArea());
        }
    }
}
using System;

namespace 反射
{
    // 一个自定义特性 BugFix 被赋给类及其成员
    [AttributeUsage(
        AttributeTargets.Class |
        AttributeTargets.Constructor |
        AttributeTargets.Field |
        AttributeTargets.Method |
        AttributeTargets.Property,
        AllowMultiple = true
    )]
    public class DebugInfo : System.Attribute
    {
        public int BugNumber { get; }
        public string Developer { get; }
        public string LastReview { get; }
        public string Message { get; set; }

        public DebugInfo(int bugNumber, string developer, string lastView)
        {
            BugNumber = bugNumber;
            Developer = developer;
            LastReview = lastView;
        }
    }
}

结果如下:

Length: 4.5
Width: 7.5
Area: 33.75
Bug Number: 45
Developer: Zara Ali
Last Reviewed: 12/8/2012
Remarks: Return type mismatch
Bug Number: 49
Developer: Nuha Ali
Last Reviewed: 10/10/2012
Remarks: Unused variable
Bug Number: 55, for Method: GetArea
Developer: Zara Ali
Last Reviewed: 19/10/2012
Remarks: Return type mismatch
Bug Number: 56, for Method: Display
Developer: Zara Ali
Last Reviewed: 19/10/2012
Remarks:

指针

不安全代码

当一个代码块使用 unsafe 修饰符标记时,C# 允许在函数中使用指针变量。不安全代码或非托管代码是指使用了指针变量的代码块。

下面的示例说明了 C# 中使用 unsafe 修饰符时指针的使用:

using System;

namespace 指针
{
    class Program
    {
        static unsafe void Main(string[] args)
        {
            int var = 20;
            int* p = &var;
            Console.WriteLine("Data is: " + var);
            Console.WriteLine("Address is: " + (int)p);
        }
    }
}

结果如下:

Data is: 20
Address is: -2068326148

使用指针检索数据值

可以使用 ToString() 方法检索存储在指针变量所引用位置的数据。

此外,可以不用声明整个方法作为不安全代码,只需要声明方法的一部分作为不安全代码。

下面的示例演示了这点:

using System;

namespace 指针
{
    class Program
    {
        static void Main()
        {
            unsafe
            {
                int var = 20;
                int* p = &var;
                Console.WriteLine("Data is: " + var);
                Console.WriteLine("Data is: " + p->ToString());
                Console.WriteLine("Address is: " + (int)p);
            }
        }
    }
}

结果如下:

Data is: 20
Data is: 20
Address is: 1301797540

当然也可以直接使用 *p,效果是一样的。

使用指针访问数组元素

在 C# 中,数组名称和一个指向与数组数据具有相同数据类型的指针是不同的变量类型。例如,int *pint[] p 是不同的类型。可以增加指针变量 p,因为它在内存中不是固定的,但是数组地址在内存中是固定的,所以不能增加数组 p

因此,如果需要使用指针变量访问数组数据(就像在 C/C++ 中所做的那样),需要使用 fixed() 语句来固定指针。

下面的示例演示了这点:

using System;

namespace 指针
{
    class Program
    {
        static void Main()
        {
            unsafe
            {
                int[] list = { 10, 100, 200 };
                fixed (int* ptr = list)
                {
                    /* 显示指针中数组地址 */
                    for (int i = 0; i < 3; i++)
                    {
                        Console.WriteLine("Address of list[" + i + "] = " + (int)(ptr + i));
                        Console.WriteLine("Value of list[" + i + "] = " + *(ptr + i));
                    }
                }
            }
        }
    }
}

结果如下:

Address of list[0] = -1312682000
Value of list[0] = 10
Address of list[1] = -1312681996
Value of list[1] = 100
Address of list[2] = -1312681992
Value of list[2] = 200

编译不安全代码

为了编译不安全代码, 需要使用 -unsafe 编译器选项。

比如,对于 Visual Studio IDE,需要在项目属性中启用不安全代码:

  1. 打开项目的「属性」页。
  2. 单击「生成」属性页。
  3. 选中「允许不安全代码」复选框。

其原理是,在项目的 .csproj 文件中,添加了以下元素:

<PropertyGroup>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

因此,如果使用其它开发环境,可以尝试直接按以上方式修改项目的 .csproj 文件。

多线程

线程(thread)被定义为程序的执行路径。每个线程都定义了一个独特的控制流。如果应用程序涉及到复杂的和耗时的操作,那么设置不同的线程执行路径往往是有益的,每个线程执行特定的工作。

线程是轻量级进程。一个使用线程的常见实例是现代操作系统中并行编程的实现。使用线程节省了 CPU 周期的浪费,同时提高了应用程序的效率。

线程生命周期

线程生命周期开始于线程对象被创建时,结束于线程被终止或完成执行时。

下面列出了线程生命周期中的各种状态:

  • 未启动状态。当线程实例被创建但 Start() 方法未被调用时的状况。
  • 就绪状态。当线程准备好运行并等待 CPU 周期时的状况。
  • 不可运行状态。下面几种情况下,线程是不可运行的:
    • 已经调用 Sleep() 方法
    • 已经调用 Wait() 方法
    • 通过 I/O 操作阻塞
  • 死亡状态。当线程已完成执行或已中止时的状况。

主线程

System.Threading.Thread 类用于线程的工作。它允许创建并访问多线程应用程序中的单个线程。进程中第一个被执行的线程称为主线程

程序开始执行时,主线程自动创建。使用 Thread 类创建的线程被主线程的子线程调用。可以使用 Thread 类的 CurrentThread 属性访问线程。

下面的示例演示了主线程的执行:

using System;
using System.Threading;

namespace 多线程
{
    class MainThread
    {
        static void Main(string[] args)
        {
            Thread th = Thread.CurrentThread;
            th.Name = "MainThread";
            Console.WriteLine("This is " + th.Name);
        }
    }
}

结果如下:

This is MainThread

创建线程

线程是通过扩展 Thread 类创建的。扩展的 Thread 类调用 Start() 方法来开始子线程的执行。

下面的示例演示了这个概念:

using System;
using System.Threading;

namespace 多线程
{
    class ThreadCreation
    {
        static void CallToChildThread()
        {
            Console.WriteLine("Child thread starts");
        }

        static void Main(string[] args)
        {
            ThreadStart childref = new ThreadStart(CallToChildThread);
            Console.WriteLine("In Main: Creating the Child thread");
            Thread childThread = new Thread(childref);
            childThread.Start();
        }
    }
}

结果如下:

In Main: Creating the Child thread
Child thread starts

管理线程

Thread 类提供了各种管理线程的方法。

下面的实例演示了 sleep() 方法的使用,用于在一个特定的时间暂停线程。

using System;
using System.Threading;

namespace 多线程
{
    public class ThreadSleep
    {
        static void CallToChildThread()
        {
            Console.WriteLine("Child thread starts");

            // 线程暂停 5000 毫秒
            int sleepfor = 5000;
            Console.WriteLine("Child Thread Paused for " + sleepfor / 1000 + " seconds");
            Thread.Sleep(sleepfor);
            Console.WriteLine("Child thread resumes");
        }

        static void Main(string[] args)
        {
            ThreadStart childref = new ThreadStart(CallToChildThread);
            Console.WriteLine("In Main: Creating the Child thread");
            Thread childThread = new Thread(childref);
            childThread.Start();
        }
    }
}

结果如下:

In Main: Creating the Child thread
Child thread starts
Child Thread Paused for 5 seconds
# 等待 5 秒钟后
Child thread resumes

销毁线程

Abort() 方法用于销毁线程。

通过抛出 ThreadAbortException 在运行时中止线程。如果有 finally 块,控制会被送至 finally 块。

下面的示例说明了这点:

using System;
using System.Threading;

namespace 多线程
{
    public class ThreadDestruction
    {
        static void CallToChildThread()
        {
            try
            {
                Console.WriteLine("Child thread starts");

                // 计数到 10
                for (int counter = 0; counter <= 10; counter++)
                {
                    Thread.Sleep(500);
                    Console.WriteLine(counter);
                }
                Console.WriteLine("Child Thread Completed");
            }
            catch (ThreadAbortException)
            {
                Console.WriteLine("Thread Abort Exception");
            }
            finally
            {
                Console.WriteLine("Couldn't catch the Thread Exception");
            }
        }

        static void Main(string[] args)
        {
            ThreadStart childref = new ThreadStart(CallToChildThread);
            Console.WriteLine("In Main: Creating the Child thread");
            Thread childThread = new Thread(childref);
            childThread.Start();
            Thread.Sleep(2000); // 停止主线程一段时间

            // 现在中止子线程
            Console.WriteLine("In Main: Aborting the Child thread");
            childThread.Abort();
        }
    }
}

结果如下:

In Main: Creating the Child thread
Child thread starts
0
1
2
In Main: Aborting the Child thread
Thread Abort Exception
Couldn't catch the Thread Exception
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容