blueyi's notes

Follow Excellence,Success will chase you!

0%

C-sharp-study-notes

参考资源

  1. msdn上的C#语言规范中文版
  2. C#中的所有关键字
  3. .NET Framework类库

Hello World

  1. C#与C/C++很多地方都很类似,对于Hello World程序来说,程序入口是Main()函数,注意首字母是大写。C#的编译环境在VS中附带的有,编译器名称为csc,C#的源程序后缀通常为.cs。例如以下程序名为hello.cs,则最简单的编译方法为csc hello.cs,会在同目录中生成hello.exe的可执行程序。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //引用一个名为System的命名空间,它由Microsoft.NET.Frameworld类库定义。该命名空间下包含`Main`方法引用的`Console`类。
    using System;
    class Hello
    {
    /*程序的入口点总是名为Main的静态方法,Main方法是Hello类的成员,它是相对于类Hello本身而不是相对于此类的实例*/
    static void Main(){
    Console.WriteLine("Hello World!");
    }
    }
  2. C#中的注释方法与C++一样。
  3. C#中的基本输入输出
    1
    2
    3
    4
    5
    using System;
    Console.Read(); // 读一个字符
    Console.ReadLine(); // 读一行
    Console.Write(); // 不换行写
    Console.WriteLine(); // 写并换行
    C#中不使用%号作为输出变量,而是使用类似python中的占位符{},eg: System.Console.Write("{0}", a);
    @相当于python中的r,即按原始文本输出。eg: System.Console.WriteLine(@"hello\tworld");输出结果为hello\tworld

基本语法

类型

  1. C#中的数据类型分为值类型和引用类型。
    值类型包括:简单类型(bool、char、int、float、double),枚举类型(),结构体类型(),有符号整型,无符号整型 等。
    引用类型包括:字符串类型(string),object类型,类类型(Class),数组类型,委托类型(),接口类型等。
    值类型即每个变量都有其自己的数据副本,引用类型则可能多个变量共用同一个对象。

  2. C#提供了一组预定义类型
    预定义的类型中,属于引用类型的有 object 和 string 两类。object 类型是所有其他类型的最终基类型。string 类型用于表示 Unicode 字符串值。string 类型的值是不可变的。
    预定义的值类型包括有符号整型、无符号整型、浮点型以及 bool、char 和 decimal 等类型。属有符号整型的有 sbyte、short、int 和 long;属无符号整型的有 byte、ushort、uint 和 ulong;属浮点型的有 float 和 double。
    bool 类型用于表示布尔值,即仅有真、假两个值。且bool类型无法与int类型进行类型转换。
    其中C/C++中没有类型为sbyte表示8位有符号整型,ushort、uint、ulong分别对应于相应的无符号型,decimal表示精确的小数类型,具有28个有效数字。
    每个预定义类型都是一个在System命名空间下的类型的简写形式。例如关键字int所指的实际上是结构类型System.Int32

  3. C#中的类型转换同样分为强制转换与隐式转换。隐式转换只用于那些不需要仔细检查就可以安全地实现转换的类型,如从int到long,eg:

    1
    2
    int intVal = 123;
    long longVal = intVal;

    与C++类似通过使用(type)来强制类型转换,eg:

    1
    2
    long longVal = Int64.MaxValue;
    int intVal = (int) longVal;
  4. 数组:
    C#中的数组类型是用一个非数组类型名称后跟一个或多个秩说明符来表示的。eg: int[]表示整型类型的数组,short[]表示短整型,其他类似。
    C#数组根据形状可分为矩形数组和交错数组。数组的元素类型和数组的形状是数组类型定义的组成部分,但数组的大小却不是数组类型定义的组成部分。C#的语法明确地规定数组的每个维的长度需要在数组创建表达式中指定。数组类型为引用类型,所以数组变量的声明只是为数组引用留出空间。数组实例是通过数组初始值设定项和数组创建表达式创建的。eg:

    1
    2
    3
    4
    int[] a; //声明一个一维的整型数组a,但并未创建数组
    int[] b = new int[5]; //声明并创建含有5个元素的一维数组b
    int[] c = new int[] {1, 2, 3}; //声明、创建并初始化一维整型数组c
    int[] d = {1, 2, 3}; //等价于c,对于局部变量和字段的声明,允许使用简写形式
  5. 多维数组
    注意C#的多维数组就是多维数组,而不是像C/C++中的多维数组就是数组的数组。而交错数组也就是数组的数组。eg:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Test
    {
    static void Main() {
    int[] a1; // single-dimensional array of int
    int[,] a2; // 2-dimensional array of int
    int[,,] a3; // 3-dimensional array of int
    int[][] j2; // "jagged" array: array of (array of int)
    int[][][] j3; // array of (array of (array of int))

    //下面为对应的初始化
    int[] a1 = new int[] {1, 2, 3};
    int[,] a2 = new int[,] {{1, 2, 3}, {4, 5, 6}}; //a2的大小为3x3
    int[,,] a3 = new int[10, 20, 30]; //a3的三维大小分别是10,20,30
    int[][] j2 = new int[3][]; //j2中含有3个int[]类型的数组,数组长度在下面声明,分别为3,6,9
    j2[0] = new int[] {1, 2, 3};
    j2[1] = new int[] {1, 2, 3, 4, 5, 6};
    j2[2] = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    }
    }

    数组在声明并创建后如果不手动初始化,各元素会被自动初始化为0。但变量必须先赋值,然后才能使用。

  6. 类型系统统一化
    C# 提供了一个“统一类型系统”。所有类型(包括值类型)都是从 object 类型派生的。这样,类型 Object 中定义的方法就可以在任何值类型的值(甚至是像 int 这样的“基元”类型的值)上调用。eg:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    using System;
    class Test
    {
    static void Main() {
    Console.WriteLine(3.ToString());
    int i = 123;
    object o = i; // boxing
    int j = (int) o; // unboxing
    }
    }

    一个 int 值可以转换为 object,并可再次转换回 int。此示例同时显示了“装箱”和“取消装箱”。当值类型的变量需要转换为引用类型时,执行“装箱”,即设置一个对象箱来保存值这个值(该值被复制到箱中)。“取消装箱”则正好相反。当对象箱被强制转换回其原来的值类型时,该值就从箱中取出并复制到适当的存储位置。

此类型系统统一化为值类型提供了对象性的优点,并且不会带来不必要的系统开销。对于不需要 int 值的行为与对象一样的程序,int 值只是 32 位值。对于需要 int 值的行为与对象一样的程序,可以根据需要使用此功能。这种将值类型作为对象处理的能力弥补了大多数语言中存在的值类型与引用类型之间的差距。例如,Stack 类可以提供 Push 和 Pop 方法,这些方法获得并返回一个 object 值。

  1. 委托:委托类型使用delegate进行声明,有一个返回值和任意数目任意类型的参数。委托是一种可用于封装命名或匿名方法的引用类型。 委托类似于 C++ 中的函数指针;但是,委托是类型安全和可靠的。 委托是事件的基础。 通过将委托与命名方法或匿名方法关联,可以实例化委托。必须使用具有兼容返回类型和输入参数的方法或 lambda 表达式实例化委托。 eg:
    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
    using System;
    // Declare delegate -- defines required signature:
    delegate double MathAction(double num);
    class DelegateTest
    {
    // Regular method that matches signature:
    static double Double(double input)
    {
    return input * 2;
    }
    static void Main()
    {
    // Instantiate delegate with named method:
    MathAction ma = Double;
    // Invoke delegate ma:
    double multByTwo = ma(4.5);
    Console.WriteLine("multByTwo: {0}", multByTwo);
    // Instantiate delegate with anonymous method:
    MathAction ma2 = delegate(double input)
    {
    return input * input;
    };
    double square = ma2(5);
    Console.WriteLine("square: {0}", square);
    // Instantiate delegate with lambda expression
    MathAction ma3 = s => s * s * s;
    double cube = ma3(4.375);
    Console.WriteLine("cube: {0}", cube);
    }
    // Output:
    // multByTwo: 9
    // square: 25
    // cube: 83.740234375
    }

变量和参数

  1. 变量必须先赋值,然后才能使用它的值。
    字段(第 10.4 节)是与类或结构或与类或结构的实例关联的变量。用 static 修饰符声明的字段定义静态变量,不用此修饰符声明的字段则定义实例变量。静态字段与类型关联,而实例变量与实例关联。
    1
    2
    3
    4
    5
    6
    7
    8
    //具有一个私有静态变量和两个公共实例变量的 Employee 类
    using Personnel.Data;
    class Employee
    {
    private static DataSet ds;
    public string Name;
    public decimal Salary;
    }
  2. C#有四种类型的参数:值参数,引用参数,输出参数和参数数组
    • 值参数:即通过传值来传递参数,即传入的变量的副本。
    • 引用参数:通过关键字ref 来声明和调用引用参数,即传递的是变量本身
    • 输出参数:通过关键字out来声明输出参数,即一个函数可以有多个输出值
    • 参数数组:即通过关键字params将一维数组作为参数传递给函数,这样可以实现可变长的参数列表

eg:
值参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
class Test
{
static void F(int p)
{
Console.WriteLine("p = {0}", p);
p++;
}
static void Main()
{
int a = 0;
Console.WriteLine("Pre: a = {0}", a);
F(a);
Console.WriteLine("New: a = {0}", a);
}
}

输出结果为全为0
引用参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
class Test
{
static void F(ref int p)
{
Console.WriteLine("p = {0}", p);
p++;
}
static void Main()
{
int a = 0;
Console.WriteLine("Pre: a = {0}", a);
F(ref a);
Console.WriteLine("New: a = {0}", a);
}
}

输出结果后面的a变成了0。

输出参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
class Test
{
static void F(ref int p, out int result)
{
Console.WriteLine("p = {0}", p);
p++;
result = p;
}
static void Main()
{
int a = 0;
int res;
Console.WriteLine("Pre: a = {0}", a);
F(ref a, out res);
Console.WriteLine("New: a = {0}, res = {1}", a, res);
}
}

显然输出参数关键字out与引用参数ref的功能非常像,其实也基本上确实是一要的。但不同的是引用参数在使用前必须初始化,而输出参数不需要,也就是在调用函数之前并没有给输出参数分配空间。就像上例中如果将int res;改为int res = 0;,则将所有的out替换为ref之后结果也完成正确。

数组参数:参数数组用 params 修饰符声明。一个给定的方法只能有一个参数数组,而且它必须始终是最后一个指定的参数。参数数组的类型总是一维数组类型。调用方可以传递一个属同一类型的数组变量,或任意多个与该数组的元素属同一类型的自变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
class Test
{
static void F(params int[] args)
{
Console.WriteLine("arguments:{0} ", args.Length);
for (int i = 0; i < args.Length; ++i)
{
Console.WriteLine("\targs[{0}]={1} ", i, args[i]);
}
}
static void Main()
{
F();
F(1, 2);
F(1, 2, 3);
F(new int []{1, 2, 3, 4});
}
}
  1. C#中的常量关键字有两个:constreadonly。 使用 const 关键字来声明某个常量字段或常量局部变量。 常量字段和常量局部变量不是变量并且不能修改, 常量可以为数字、布尔值、字符串或 null 引用。 readonly关键字是可以在字段上使用的修饰符。 当字段声明包括 readonly修饰符时,该声明引入的字段赋值只能作为声明的一部分出现,或者出现在同一类的构造函数中。
    它们的不同之处: const 字段只能在该字段的声明中初始化,且编译时确定。 readonly 字段可以在声明或构造函数中初始化。 因此,根据所使用的构造函数,readonly 字段可能具有不同的值。即readonly声明的变量可以在声明之后赋值但在运行时是确定的。

  2. C#的命令行参数与C/C++不同,它的第一个参数不是程序名,而是你输入的第一个参数。

表达式

C#中除了C/C++常见的表达式外,增加了checked, unchecked, (T)x, is, as等。

  1. checked, unchecked: checked 和 unchecked关键字用来限定检查或者不检查数学运算溢出的;如果使用了checked发生数学运算溢出时会抛出OverflowException;如果使用了unchecked则不会检查溢出,算错了也不会报错。eg:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int a = int.MaxValue * 2;  //编译报错
    int temp = int.MaxValue;
    int a = temp * 2; //编译通过,显然结果不正确,因为编译时的溢出检查仅限于使用常量的表达式。
    //使用checked溢出检查
    int temp = int.MaxValue;
    try {
    int a = checked(temp * 2); //checked也可以用于代码块
    }
    catch (OverflowException) {
    Console.WriteLine("OverFlow");
    }
    //使用unchecked不检查溢出
    int a = unchecked(int.MaxValue * 2); //这种不检查的可以用于不需要正确结果的计算,比如hasecode
  2. (T)x:

  3. is : is检查一个对象是否兼容于指定的类型,即提供类型检查功能。如果所提供的表达式非空,并且所提供的对象可以强制转换为所提供的类型而不会导致引发异常,则 is 表达式的计算结果将是 true。注意,is操作符永远不会抛出异常,且如果对象引用是null时,is操作符总是返回false eg:

    1
    2
    Object a = new Object();
    Boolean b = (a is Object); // b is true
  4. as: 提供检查并转换为可兼容的类型,如果类型相同,就返回一个非空的引用,否则就返回一个空引用。以下isas实现的功能相同,但使用as的性能更好。eg:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //此处需要首先检查一次,然后在强制转换时CLR会再次进行检查类型是否兼容
    if (o is Employee) {
    Employee e = (Employee) o;
    //对e进行操作
    }

    //此处只需要一次类型检查并转换
    Employee e = o as Employee;
    if (e != null) {
    //对e进行操作
    }

    语句

    C#中增加的语句有:

  5. foreach, in语句:功能是遍历array或者object collection中的元素,使用方法为foreach (type identifier in expression) statement;,该语句在使用时应只读数据,避免在遍历过程中对数据进行修改,以免出现不可预测的结果。 eg:

    1
    2
    3
    4
    static void Main(string[] args) {
    foreach (string s in args)
    Console.WriteLine(s);
    }
  6. checkedunchecked 语句 eg:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static void Main() {
    int x = Int32.MaxValue;

    Console.WriteLine(x + 1); // Overflow

    checked {
    Console.WriteLine(x + 1); // Exception
    }

    unchecked {
    Console.WriteLine(x + 1); // Overflow
    }
    }
  7. lock语句:关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。 lock 关键字可确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。 如果其他线程尝试进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。官方示例

    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
    //只要lock语句存在,语句块就是临界区并且balance永远不会是负数
    // using System.Threading;
    class Account
    {
    private Object thisLock = new Object();
    int balance;

    Random r = new Random();

    public Account(int initial)
    {
    balance = initial;
    }
    int Withdraw(int amount)
    {
    // This condition never is true unless the lock statement
    // is commented out.
    if (balance < 0)
    {
    throw new Exception("Negative Balance");
    }
    // Comment out the next line to see the effect of leaving out
    // the lock keyword.
    lock (thisLock)
    {
    if (balance >= amount)
    {
    Console.WriteLine("Balance before Withdrawal : " + balance);
    Console.WriteLine("Amount to Withdraw : -" + amount);
    balance = balance - amount;
    Console.WriteLine("Balance after Withdrawal : " + balance);
    return amount;
    }
    else
    {
    return 0; // transaction rejected
    }
    }
    }

    public void DoTransactions()
    {
    for (int i = 0; i < 100; i++)
    {
    Withdraw(r.Next(1, 100));
    }
    }
    }

    class Test
    {
    static void Main()
    {
    Thread[] threads = new Thread[10];
    Account acc = new Account(1000);
    for (int i = 0; i < 10; i++)
    {
    Thread t = new Thread(new ThreadStart(acc.DoTransactions));
    threads[i] = t;
    }
    for (int i = 0; i < 10; i++)
    {
    threads[i].Start();
    }
    }
    }
  8. using语句:
    using 关键字有两个主要用途:
    作为指令,用于为命名空间创建别名或导入其他命名空间中定义的类型。
    using 指令有三种用途:

    允许在命名空间中使用类型,这样无需在该命名空间中限定某个类型的使用:  
    `using System.Text;`
    允许访问类型的静态成员,而无需限定使用类型名称进行访问:  
    `using static System.Math;`
    为命名空间或类型创建别名。 这称为 using 别名指令。  
    `using Project = PC.MyCompany.Project;`

    作为语句,用于定义一个范围,在此范围的末尾将释放对象。提供能确保正确使用 IDisposable 对象的方便语法。

    1
    2
    3
    4
    using (Font font1 = new Font("Arial", 10.0f)) 
    {
    byte charset = font1.GdiCharSet;
    }

自动内存管理

C#提供自动内存管理,也就是说当某些变量或者对象不再需要时,会被垃圾回收器自动回收。当需要精确的内存控制时则需要使用关键字unsafe来创建不安全代码快,从而可以在其中编写直接操纵内存的代码。例如直接处理指针类型和对象地址。eg:

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
using System;
class Test
{
static void WriteLocations(byte[] arr)
{
unsafe
{
fixed (byte* pArray = arr)
{
byte* pElem = pArray;
for (int i = 0; i < arr.Length; i++)
{
byte value = *pElem;
Console.WriteLine("arr[{0}] at 0x{1:X} is {2}",
i, (uint)pElem, value);
pElem++;
}
}
}
}
static void Main()
{
byte[] arr = new byte[] { 1, 2, 3, 4, 5 };
WriteLocations(arr);
}
}

编译时记得添加/unsafe参数。VS则需要在项目属性中勾选“允许不安全代码”。
fixed 语句禁止垃圾回收器重定位可移动的变量。 fixed 语句只在不安全的上下文中是允许的。 Fixed 还可用于创建固定大小缓冲区。 fixed 语句设置指向托管变量的指针,并在执行该语句期间“固定”此变量。 如果没有 fixed 语句,则指向可移动托管变量的指针的作用很小,因为垃圾回收可能不可预知地重定位变量。 C# 编译器只允许在 fixed 语句中分配指向托管变量的指针。

  1. 类成员可以包括:常数、字段、方法、属性、事件、索引器、运算符、实例构造函数、析构函数、静态构造函数和嵌套类型声明。每个成员都有关联的可访问性,一共有5种可访问性,分别为:
  • public 不限制访问。
  • protected 访问限于该成员所属的类或从该类派生来的类型。
  • internal 访问限于此程序。
  • protected internal 访问限于此程序或从该成员所属的类派生的类型。
  • private 访问限于该成员所属的类型。
  • partial 分部类型定义允许将类、结构或接口的定义拆分到多个文件中。
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
using System;
class MyClass
{
public MyClass() {
Console.WriteLine("Instance constructor");
}
public MyClass(int value) {
MyField = value;
Console.WriteLine("Instance constructor");
}
~MyClass() {
Console.WriteLine("Destructor");
}
public const int MyConst = 12; //常数成员,即可以在编译时确定的值
public int MyField = 34;
public void MyMethod(){
Console.WriteLine("MyClass.MyMethod");
}
public int MyProperty {
get {
return MyField;
}
set {
MyField = value;
}
}
public int this[int index] {
get {
return 0;
}
set {
Console.WriteLine("this[{0}] = {1}", index, value);
}
}
public event EventHandler MyEvent;
public static MyClass operator+(MyClass a, MyClass b) {
return new MyClass(a.MyField + b.MyField);
}
internal class MyNestedClass
{}
}

class Test
{
static void Main() {
// Instance constructor usage
MyClass a = new MyClass();
MyClass b = new MyClass(123);
// Constant usage
Console.WriteLine("MyConst = {0}", MyClass.MyConst);
// Field usage
a.MyField++;
Console.WriteLine("a.MyField = {0}", a.MyField);
// Method usage
a.MyMethod();
// Property usage
a.MyProperty++;
Console.WriteLine("a.MyProperty = {0}", a.MyProperty);
// Indexer usage
a[3] = a[1] = a[2];
Console.WriteLine("a[3] = {0}", a[3]);
// Event usage
a.MyEvent += new EventHandler(MyHandler);
// Overloaded operator usage
MyClass c = a + b;
}
static void MyHandler(object sender, EventArgs e) {
Console.WriteLine("Test.MyHandler");
}
internal class MyNestedClass
{}
}
  1. 字段: 字段是一种成员,它表示与对象或类相关联的一个变量。 readonly 关键字是可以在字段上使用的修饰符。 当字段声明包括 readonly 修饰符时,该声明引入的字段赋值只能作为声明的一部分出现,或者出现在同一类的构造函数中。静态 readonly 字段可以在静态构造函数中赋值,而非静态 readonly 字段可以在实例构造函数中赋值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Color
    {

    //4个实例字段
    public static readonly Color Red = new Color(0xFF, 0, 0);
    public static readonly Color Blue = new Color(0, 0xFF, 0);
    public static readonly Color Green = new Color(0, 0, 0xFF);
    public static readonly Color White = new Color(0xFF, 0xFF, 0xFF);
    //声明3个内部实例字段
    internal ushort redPart;
    internal ushort bluePart;
    internal ushort greenPart;
    public Color(ushort red, ushort blue, ushort green) {
    redPart = red;
    bluePart = blue;
    greenPart = green;
    }
    }
    字段可以直接在声明类的时候初始化。eg:
    1
    Color newCol = new Color() {redPart = red, bluePart = blue};
  1. 方法:方法是一种成员,它用于实现可由对象或类执行的计算或操作。方法有一个形参表(可能是空的)、一个返回值(除非方法的返回类型为 void),它不是静态的,就是非静态的。通过类访问静态方法。通过类的实例访问非静态方法(也称为实例方法)。 要扩展或修改继承的方法、属性、索引器或事件的抽象实现或虚实现,必须使用 override 修饰符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    using System;
    public class Stack
    {
    //只有静态方法才可以直接通过类访问,非静态方法必须首先创建实例,然后通过实例调用。
    public static Stack Clone(Stack s) {...}
    public static Stack Flip(Stack s) {...}
    public object Pop() {...}
    public void Push(object o) {...}
    public override string ToString() {...}
    ...
    }
    class Test
    {
    static void Main() {
    Stack s = new Stack();
    for (int i = 1; i < 10; i++)
    s.Push(i);
    Stack flipped = Stack.Flip(s);
    Stack cloned = Stack.Clone(s);
    Console.WriteLine("Original stack: " + s.ToString());
    Console.WriteLine("Flipped stack: " + flipped.ToString());
    Console.WriteLine("Cloned stack: " + cloned.ToString());
    }
    }
  2. 属性: 属性是一种成员,可用它来访问对象或类的某个特性。属性的示例包括字符串的长度、字体的大小、窗口的标题、客户的名称,等等。属性是字段的自然扩展。属性和字段都是命名的成员,都具有相关的类型,且用于访问字段和属性的语法也相同。属性用属性声明定义。属性声明的第一部分看上去与字段声明非常相似。第二部分包含一个 get 访问器和/或一个 set 访问器。可读取并写入的属性同时包含 get 和 set 访问器。当读取属性值时调用 get 访问器;当写入属性值时则调用 set 访问器。在 set 访问器中,属性的新值是通过一个名为 value 的隐式参数来赋值的。 属性声明相对直接一些,但是属性的实际值在它们被使用时才可见。在下面的示例中,Button 类定义一个 Caption 属性。 读取和写入 Caption 属性的方式可以与读取和写入字段相同:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Button 
    {
    private string caption;
    public string Caption {
    get {
    return caption;
    }
    set {
    caption = value;
    Repaint();
    }
    }
    }
    Button b = new Button();
    b.Caption = "ABC"; // set; causes repaint
    string s = b.Caption; // get
    b.Caption += "DEF"; // get & set; causes repaint
  3. 事件: 事件是一种成员,对象或类能够通过它发送通知。类通过提供事件声明来定义事件。事件声明类似于字段声明,但它增加了 event 关键字和一组可选的事件访问器。用于声明事件的类型必须是某种委托类型。
    一个委托类型的实例封装了一个或多个“可调用实体”。对于实例方法,可调用实体由一个实例和该实例的方法组成。对于静态方法,可调用实体仅由一个方法组成。当用一组合适的参数调用某个委托实例时,该委托实例所封装的每个可调用实体都会被逐个调用,且都使用给定的同一组参数。如下实例中, Button 类定义一个 EventHandler 类型的 Click 事件。在 Button 类的内部,Click 成员如同一个 EventHandler 类型的私有字段。而在 Button 类的外部,Click 成员只能用在 += 和 -= 运算符的左侧。+= 运算符为 Click 事件添加一个事件处理程序,而 -= 运算符则为它移除一个事件处理程序。

    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
    using System;
    public delegate void EventHandler(object sender, System.EventArgs e); //delegate 用户定义委托
    public class Button
    {
    public event EventHandler Click;
    public void Reset()
    {
    Click = null;
    }
    }
    public class Form1
    {
    public Form1()
    {
    // Add Button1_Click as an event handler for Button1's Click event
    Button1.Click += new EventHandler(Button1_Click);
    }
    Button Button1 = new Button();
    void Button1_Click(object sender, EventArgs e)
    {
    Console.WriteLine("Button1 was clicked!");
    }
    public void Disconnect()
    {
    Button1.Click -= new EventHandler(Button1_Click);
    }
    static void Main()
    {
    Form1 f = new Form1();
    }
    }
  4. 运算符:运算符是一种成员,它用来定义表达式运算符的含义,使其能应用于属于类的实例。可以定义三种运算符:一元运算符、二元运算符和转换运算符。所有运算符都必须声明为公共的和静态的。
    下面的示例定义一个表示十进制数字(介于 0 和 9 之间的整数值)的 Digit 类型。
    此 Digit 类型定义下列运算符:

    • 从 Digit 到 byte 的隐式转换运算符。
    • 从 byte 到 Digit 的显式转换运算符。
    • 将两个 Digit 值相加并返回一个 Digit 值的加法运算符。
    • 从一个 Digit 值中减去另一个 Digit 值并返回一个 Digit 值的减法运算符。
    • 相等 (==) 和不相等 (!=) 运算符,它们对两个 Digit 值进行比较。
      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
      using System;
      public struct Digit
      {
      byte value;
      public Digit(byte value) {
      if (value < 0 || value > 9) throw new ArgumentException();
      this.value = value;
      }
      public Digit(int value): this((byte) value) {}
      public static implicit operator byte(Digit d) {
      return d.value;
      }
      public static explicit operator Digit(byte b) {
      return new Digit(b);
      }
      public static Digit operator+(Digit a, Digit b) {
      return new Digit(a.value + b.value);
      }
      public static Digit operator-(Digit a, Digit b) {
      return new Digit(a.value - b.value);
      }
      public static bool operator==(Digit a, Digit b) {
      return a.value == b.value;
      }
      public static bool operator!=(Digit a, Digit b) {
      return a.value != b.value;
      }
      public override bool Equals(object value) {
      if (value == null) return false;
      if (GetType() == value.GetType()) return this == (Digit)value;
      return false;
      }
      public override int GetHashCode() {
      return value.GetHashCode();
      }
      public override string ToString() {
      return value.ToString();
      }
      }
      class Test
      {
      static void Main() {
      Digit a = (Digit) 5;
      Digit b = (Digit) 3;
      Digit plus = a + b;
      Digit minus = a - b;
      bool equals = (a == b);
      Console.WriteLine("{0} + {1} = {2}", a, b, plus);
      Console.WriteLine("{0} - {1} = {2}", a, b, minus);
      Console.WriteLine("{0} == {1} = {2}", a, b, equals);
      }
      }
      operator关键字:使用 operator 关键字来重载内置运算符,或提供类或结构声明中的用户定义转换。
  5. 索引器:索引器是一种成员,它使对象能够用与数组相同的方式进行索引。属性启用类似字段的访问,而索引器启用类似数组的访问。
    例如,请看一下前面研究过的 Stack 类。该类的设计者可能想提供类似数组的访问,以便不必执行 Push 和 Pop 操作,就可以检查或改变堆栈上的各个项。也就是说,使 Stack 类既是链接表,又可像数组一样方便地对它进行访问。
    索引器声明类似于属性声明,主要区别是索引器是无名称的(由于 this 被索引,因此在声明中使用的“名称”为 this),而且索引器包含索引参数。索引参数在方括号中提供。示例

    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
    using System;
    public class Stack
    {
    private Node GetNode(int index) {
    Node temp = first;
    while (index > 0 && temp != null) {
    temp = temp.Next;
    index--;
    }
    if (index < 0 || temp == null)
    throw new Exception("Index out of range."); return temp;
    }
    public object this[int index] {
    get {
    return GetNode(index).Value;
    }
    set {
    GetNode(index).Value = value;
    }
    }
    ...
    }
    class Test
    {
    static void Main() {
    Stack s = new Stack();
    s.Push(1);
    s.Push(2);
    s.Push(3);
    s[0] = 33; // Changes the top item from 3 to 33
    s[1] = 22; // Changes the middle item from 2 to 22
    s[2] = 11; // Changes the bottom item from 1 to 11
    }
    }
  6. 实例构造函数是一种成员,用来实现初始化一个类的实例时所需的操作。如果没有为某个类提供任何实例构造函数,则将自动提供一个不带参数的空实例构造函数。

    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
    using System;
    class Point
    {
    public double x, y;
    //无参数的构造函数
    public Point() {
    this.x = 0;
    this.y = 0;
    }
    //带参数的构造函数
    public Point(double x, double y) {
    this.x = x;
    this.y = y;
    }
    public static double Distance(Point a, Point b) {
    double xdiff = a.x + b.x;
    double ydiff = a.y + b.y;
    return Math.Sqrt(xdiff * xdiff + ydiff * ydiff);
    }
    public override string ToString() {
    return string.Format("({0}, {1})", x, y);
    }
    }
    class Test
    {
    static void Main() {
    Point a = new Point();
    Point b = new Point(3, 4);
    double d = Point.Distance(a, b);
    Console.WriteLine("Distance from {0} to {1} is {2}", a, b, d);
    }
    }
  7. 析构函数是一种成员,用来实现析构一个类实例所需的操作。析构函数不能带参数,不能具有可访问性修饰符,也不能被显式调用。垃圾回收期间会自动调用所涉及实例的析构函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    using System;
    class Point
    {
    public double x, y;
    public Point(double x, double y) {
    this.x = x;
    this.y = y;
    }
    //析构函数
    ~Point() {
    Console.WriteLine("Destructed {0}", this);
    }
    public override string ToString() {
    return string.Format("({0}, {1})", x, y);
    }
    }
  8. 静态构造函数是一种成员,用来实现初始化一个类所需的操作。静态构造函数不能带参数,不能具有可访问性修饰符,也不能被显式调用。类的静态构造函数是自动地被调用的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    using Personnel.Data;
    class Employee
    {
    private static DataSet ds;
    static Employee() {
    ds = new DataSet(...);
    }
    public string Name;
    public decimal Salary;
    ...
    }
  9. 继承:类支持单一继承,而类型 object 是所有类的最终基类。 也就是说只要你声明了一个新类,默认地他继承于object类。eg:

    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
    using System;
    //类A隐式地从object派生的类A。
    class A
    {
    public void F() { Console.WriteLine("A.F"); }
    }

    //类B显式地继承于A,即从A派生的类B,B也继承了A的F方法,并添加了方法G
    class B: A
    {
    public void G() { Console.WriteLine("B.G"); }
    }
    class Test
    {
    static void Main() {
    B b = new B();
    b.F(); // Inherited from A
    b.G(); // Introduced in B
    A a = b; // Treat a B as an A
    a.F();
    }
    }

    //显示了一个包含虚方法 F 的类 A 和一个类 B(它重写了 F)。B 中的重写方法包含一个调用 base.F(),它调用 A 中被重写的方法。 一个类可以使用 abstract 修饰符来指示它自己是不完整的,只打算用作其他类的基类。这样的类称为抽象类。抽象类可以指定抽象成员,即非抽象派生类必须实现的成员。

    //在抽象类 A 中引入了抽象方法 F。非抽象类 B 提供此方法的实现。
    abstract class A
    {
    public abstract void F();
    }
    class B: A
    {
    public override void F() { Console.WriteLine("B.F"); }
    }
    class Test
    {
    static void Main() {
    B b = new B();
    b.F();
    A a = b;
    a.F();
    }
    }

    要扩展或修改继承的方法、属性、索引器或事件的抽象实现或虚实现,必须使用override修饰符。

  10. 函数的默认可见必为privateinternal为命名空间内可见。

结构 

类与结构有很多相似之处:结构可以实现接口,并且可以具有与类相同的成员类型。然而,结构在几个重要方面不同于类:结构为值类型而不是引用类型,并且结构不支持继承。结构的值存储在“在堆栈上”或“内联”。细心的程序员有时可以通过聪明地使用结构来增强性能。 eg:

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
//通过类实现的Point
class Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
class Test
{
static void Main() {
Point[] points = new Point[100];
for (int i = 0; i < 100; i++)
points[i] = new Point(i, i*i);
}
}

//通过结构实现的Point
//如果将其应用于上述Test,则只出现一个实例对象(用于数组的对象)。Point 实例在数组中内联分配。此优化可能会被误用。使用结构而不是类还会使应用程序运行得更慢或占用更多的内存,因为将结构实例作为值参数传递会导致创建结构的副本。
struct Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}

接口

接口使用关键字interface定义,一个接口定义一个协定。实现接口的类或结构必须遵守其协定。接口可以包含方法、属性、索引器和事件作为成员。 实现接口的类或结构必须实现接口定义中指定的接口成员。 接口可以使用多重继承。类和结构可以实现多个接口。

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
//声明了一个包含索引器、事件E、方法F和属性P的接口
interface IExample
{
string this[int index] { get; set; }
event EventHandler E;
void F(int value);
string P { get; set; }
}
public delegate void EventHandler(object sender, EventArgs e);

//接口实现的多重继承,接口 IComboBox 同时从 ITextBox 和 IListBox 继承。
interface IControl
{
void Paint();
}
interface ITextBox: IControl
{
void SetText(string text);
}
interface IListBox: IControl
{
void SetItems(string[] items);
}
interface IComboBox: ITextBox, IListBox {}

//类实现的多个接口,类 EditBox 从类 Control 派生,并且同时实现 IControl 和 IDataBound。
interface IDataBound
{
void Bind(Binder b);
}
public class EditBox: Control, IControl, IDataBound
{
public void Paint() {...}
public void Bind(Binder b) {...}
}
/*
在前面的示例中,IControl 接口中的 Paint 方法和 IDataBound 接口中的 Bind 方法是使用 EditBox 类的公共成员实现的。
C# 提供了另一种方式来实现这些方法,使得实现类避免将这些成员设置成公共的。
这就是:接口成员可以用限定名来实现。例如,在 EditBox 类中将 Paint 方法命名为
IControl.Paint,将 Bind 方法命名为 IDataBound.Bind 方法。
*/
public class EditBox: IControl, IDataBound
{
void IControl.Paint() {...}
void IDataBound.Bind(Binder b) {...}
}
/*
用这种方式实现的接口成员称为显式接口成员,这是因为每个成员都显式地指定要实现的接口成员。
显式接口成员只能通过接口来调用。例如,在 EditBox 中实现的 Paint
方法只能通过强制转换为 IControl 接口来调用。测试如下
*/
class Test
{
static void Main() {
EditBox editbox = new EditBox();
editbox.Paint(); // error: no such method
IControl control = editbox;
control.Paint(); // calls EditBox's Paint implementation
}
}

委托

委托使用关键字delegate声明。委托适用于那种在某些其他语言中需用函数指针来解决的情况(场合)。但是,与函数指针不同,委托是面向对象和类型安全的。 委托声明定义一个类,它是从 System.Delegate 类派生的类。委托实例封装了一个调用列表,该列表列出了一个或多个方法,每个方法称为一个可调用实体。对于实例方法,可调用实体由一个实例和该实例的方法组成。对于静态方法,可调用实体仅由一个方法组成。如果用一组合适的参数来调用一个委托实例,则该委托实例所封装的每个可调用实体都会被调用,并且用的都是上述的同一组参数。
委托实例的一个有趣且有用的属性是:它既不知道也不关心有关它所封装的方法所属的类的种种详情;对它来说最重要的是这些方法与该委托的类型兼容。

定义和使用委托分三个步骤:声明、实例化和调用。 eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//声明一个名为SimpleDelegate的委托
delegate void SimpleDelegate();
class Test
{
static void F() {
System.Console.WriteLine("Test.F");
}
static void Main() {
//创建一个委托实例并调用
SimpleDelegate d = new SimpleDelegate(F);
d();
}
}
//重复调用委托委托的方法
void MultiCall(SimpleDelegate d, int count) {
for (int i = 0; i < count; i++) {
d();
}
}

枚举

与C/C++中一样,没什么可说的。枚举类型声明为一组相关的符号常数定义了一个类型名称。枚举用于“多项选择”场合,就是程序运行时从编译时已经设定的固定数目的“选择”中做出决定。使用枚举的好处使用枚举胜过使用整数常数(在没有枚举的语言中很常见),这是因为使用枚举使代码更具可读性和自归档。注意枚举其实就是多选一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum Color 
{
Red,
Blue,
Green
}
class Shape
{
public void Fill(Color color) {
switch(color) {
case Color.Red:
...
break;
case Color.Blue:
...
break;
case Color.Green:
...
break;
default:
break;
}
}
}

命名空间和程序集

命名空间和程序集有助于开发基于组件的系统。命名空间提供一个逻辑组织体系。命名空间既用作程序的“内部”组织体系,也用作“外部”组织体系(一种表示向其他程序公开程序元素的途径)。

程序集用于物理打包和部署。程序集可以包含类型、用于实现这些类型的可执行代码以及对其他程序集的引用。

有两种主要的程序集:应用程序和库。应用程序有一个主入口点,通常具有 .exe 文件扩展名;而库没有主入口点,通常具有 .dll 文件扩展名。将hello world程序分成dll和exe的实例主要分成两步:编写dll组件和编写调用dll组件的应用程序。

  1. HelloLibrary.dll的源程序:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // HelloLibrary.cs
    namespace Microsoft.CSharp.Introduction
    {
    public class HelloMessage
    {
    public string Message {
    get {
    return "hello, world";
    }
    }
    }
    }

将其编译为.dll的方法为csc /target:library HelloLibrary.cs,会生成一个名为HelloLibrary.dll的文件。
关于命名空间的组织方式中

1
2
3
namespace Microsoft.CSharp.Introduction
{...}

等价于:

1
2
3
4
5
6
7
8
namespace Microsoft
{
namespace CSharp
{
namespace Introduction
{...}
}
}
  1. 将“hello, world”组件化的下一个步骤是编写使用 HelloMessage 类的控制台应用程序。可以使用此类的完全限定名 Microsoft.CSharp.Introduction.HelloMessage,但该名称太长,使用起来不方便。一种更方便的方法是使用“using 命名空间指令”,这样,使用相应的命名空间中的所有类型时就不必加限定名称。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // HelloApp.cs
    using Microsoft.CSharp.Introduction;
    class HelloApp
    {
    static void Main() {
    HelloMessage m = new HelloMessage();
    System.Console.WriteLine(m.Message);
    }
    }
    调用.dll的编译方法为csc /reference:HelloLibrary.dll HelloApp.cs,编译后会生成一个HelloApp.exe的可执行文件。

C# 还允许定义和使用别名。using 别名指令定义类型的别名。当两个类库之间发生名称冲突时,或者当使用大得多的命名空间中的少数类型时,这类别名很有用。using MessageSource = Microsoft.CSharp.Introduction.HelloMessage;

这样就可以将Hello World程序组件化,只有将相应的.dll文件和.exe放入同一文件夹下才能正常运行。

版本控制

版本控制是一个过程,它以兼容的方式对组件进行不断的改进。如果依赖于早期版本的代码重新编译后可以适用于新版本,则组件的新版本与早期版本源代码兼容。相反,如果依赖于早期版本的应用程序不用重新编译即可适用于新版本,则组件的新版本为二进制兼容。
使用new关键字和override关键字来明确对父类方法的隐藏或者重写。eg:

1
2
3
4
5
6
7
new public virtual void F() {
System.Console.WriteLine("Derived.F");
}
public override void F() {
base.F();
System.Console.WriteLine("Derived.F");
}

属性

其他总结

字符串

  1. 所有String类方法官方链接:https://msdn.microsoft.com/zh-cn/library/system.string.aspx

Welcome to my other publishing channels