C# 学习笔记(三)

类和继承

类继承和访问继承成员

继承的概念和形式基本和 C++ 相同,访问继承成员的形式也和访问普通成员一样。

所有类都派生自 object 类

与 C++ 不同的是,C# 中除了特殊的类 object,所有类都是派生类,即使它们没有基类规格说明。类 object 是唯一的非派生类,因为它是继承层次结构的基础。

没有基类规格说明的类都隐式的派生自类 object。不加基类规格说明只是指定 object 为基类的简写。

  • 一个类声明的基类规格说明中只能有一个单独的类。这称为单继承
  • 虽然类只能直接继承自一个基类,但派生的层次没有限制。也就是说,作为基类的类可以派生自另外一个类,而这个类又派生自另外一个类……,直至最终达到 object

基类和派生类是相对的术语。所有类都是派生类,要么派生自 object,要么派生自其他类。所以通常称一个类为派生类时,我们的意思是它直接派生自某类,而不是 object

屏蔽基类的成员

和 C++ 的同名覆盖原则类似,C# 派生类的同名成员会覆盖基类的,不过与 C++ 不同的是,C# 的方法只有函数签名完全相同时才会覆盖,单纯的名称相同只会重载。

  • 书中提到要屏蔽一个继承的数据成员,需要声明一个新的相同类型的成员,并使用相同的名称。(但实际测试下来,只需要名称相同,编译器就会认为是屏蔽了基类的同名字段,毕竟字段没有重载概念)
  • 通过在派生类中声明带有相同签名的函数成员,可以屏蔽继承的函数成员。(只同名的话,就是单纯的函数重载)
  • 要让编译器知道你故意屏蔽继承的成员,可使用 new 修饰符。否则,程序可以成功编译,但编译器会警告你隐藏了一个继承的成员
  • 也可以屏蔽静态成员
class SomeClass
{
    public string Field1 = "SomeClass Field1";

    public void Method1(string value)
    {
        Console.WriteLine($"SomeClass.Method1: {value}");
    }
}

class OtherClass : SomeClass
{
    public new string Field1 = "OtherClass Field1";

    public new void Method1(string value)
    {
        Console.WriteLine($"OtherClass.Method1: {value}");
    }
}

class Program
{
    static void Main()
    {
        OtherClass oc = new OtherClass();
        oc.Method1(oc.Field1);
    }
}
// OtherClass.Method1: OtherClass Field1

基类访问

如果派生类必须访问被隐藏的继承成员,可以使用基类访问表达式。基类访问表达式由关键字 base 后面跟着一个点和成员名称组成。和 C++ 不同,这种方式只能在派生类内访问其直接的父类中被覆盖的成员,在类外无法使用,也不能访问到更高级的祖先类中被覆盖的成员。

class SomeClass
{
    public string Field1 = "SomeClass Field1";
}

class OtherClass : SomeClass
{
    public new string Field1 = "OtherClass Field1";
}

class TestClass : OtherClass
{
    public new string Field1 = "TestClass Field1";

    public void PrintField1()
    {
        Console.WriteLine(Field1);
        Console.WriteLine(base.Field1);
    }
}

class Program
{
    static void Main()
    {
        TestClass tc = new TestClass();
        tc.PrintField1();
    }
}
// TestClass Field1
// OtherClass Field1

使用基类的引用

和 C++ 相同,C# 也可以将派生类对象的引用转换为基类的引用,此时只能访问到基类中的成员。

class MyBaseClass
{
    public void Print()
    {
        Console.WriteLine("This is the Base Class");
    }
}

class MyDerivedClass : MyBaseClass
{
    public int val1;

    public void Print()
    {
        Console.WriteLine("This is the Derived Class");
    }
}

class Program
{
    static void Main()
    {
        MyDerivedClass derived = new MyDerivedClass();
        MyBaseClass mybc = (MyBaseClass)derived; // 类型转换可以省略,是会隐式自动转换的

        derived.Print();
        mybc.Print();
        // mybc.val1 = 10; // 错误:基类引用无法访问派生类成员
    }
}
// This is the Derived Class
// This is the Base Class

虚方法和覆写方法

基本和 C++ 虚函数相同,当满足以下条件时,可以使用基类引用调用派生类的方法

  • 派生类的方法和基类的方法有相同的签名和返回类型
  • 基类的方法使用 virtual 标注
  • 派生类的方法使用 overrride 标注(这里和 C++ 不同,C++ 的 override 修饰符是可加可不加的,C# 不加就没有效果)
class MyBaseClass
{
    public virtual void Print()
    {
        Console.WriteLine("This is the Base Class");
    }
}

class MyDerivedClass : MyBaseClass
{
    public override void Print()
    {
        Console.WriteLine("This is the Derived Class");
    }
}

class Program
{
    static void Main()
    {
        MyDerivedClass derived = new MyDerivedClass();
        MyBaseClass mybc = derived;

        derived.Print();
        mybc.Print();
    }
}
// This is the Derived Class
// This is the Derived Class

和 C++ 类似,覆写和被覆写的方法也有要求:

  • 覆写和被覆写的方法必须有相同的可访问性
  • 不能覆写 static 方法或非虚方法
  • 方法、属性和索引器,以及另一种成员类型——事件,都可以被声明为 virtualoverride

覆写标记为 override 的方法

C++ 会一路调用到最高派生类的中重写的方法,C# 由于有 override 关键字,因此可以中间截断,只要中间有一个没有声明为 override 就会停止

class MyBaseClass
{
    public virtual void Print()
    {
        Console.WriteLine("This is the Base Class");
    }
}

class MyDerivedClass : MyBaseClass
{
    public override void Print()
    {
        Console.WriteLine("This is the Derived Class");
    }
}

class SecondDerived : MyDerivedClass
{
    public override void Print() // 这里如果没有 override 或者是 new,那么最后调用的就是 MyDerivedClass 的版本
    {
        Console.WriteLine("This is the Second Derived Class");
    }
}

class Program
{
    static void Main()
    {
        SecondDerived derived = new SecondDerived();
        MyBaseClass mybc = derived;

        derived.Print();
        mybc.Print();
    }
}
// This is the Second Derived Class
// This is the Second Derived Class

覆盖其他成员类型

在属性上使用 virtual/override

class MyBaseClass
{
    private int _myInt = 5;

    public virtual int MyProperty
    {
        get => _myInt;
    }
}

class MyDerivedClass : MyBaseClass
{
    private int _myInt = 10;

    public override int MyProperty
    {
        get => _myInt;
    }
}

class Program
{
    static void Main()
    {
        MyDerivedClass derived = new MyDerivedClass();
        MyBaseClass mybc = derived;

        Console.WriteLine(derived.MyProperty);
        Console.WriteLine(mybc.MyProperty);
    }
}
// 10
// 10

构造函数的执行

和 C++ 相同,派生类的构造函数执行前默认会调用基类的无参构造函数,不过需要注意成员的初始化是发生在基类构造函数执行之前的。

和 C++ 一样,在基类构造函数中调用虚函数是错误的行为,此时派生类部分还没完全初始化

构造函数初始化语句

C# 也有类似于 C++ 构造函数初始化列表的东西用于显式的调用基类的构造函数

有两种形式的构造函数初始化语句

  • 使用 base 关键字并指明使用哪一个基类构造函数,用法和 C++ 的类似

    class MyDerivedClass : MyBaseClass
    {
        public MyDerivedClass(int x, string s) : base(s, x) // 必须和基类的构造函数参数列表匹配 public MyBaseClass(string s, int x)
        {
            ...
        }
        ...
    }
    
  • 第二个形式是是使用关键字 this 并指明应当使用当前类的哪个构造函数,这种形式是为在复用当前类的其他构造函数

    class MyClass
    {
        private readonly int firstVar;
        private readonly double secondVar;
    
        public string UserName;
        public int UserIdNumber;
    
        private MyClass()
        {
            firstVar = 20;
            secondVar = 30.5;
        }
    
        public MyClass(string firstName) : this()
        {
            UserName = firstName;
            UserIdNumber = -1;
        }
    
        public MyClass(int idNumber) : this()
        {
            UserName = "Anonymous";
            UserIdNumber = idNumber;
        }
    }
    

类访问修饰符

类可以被系统中的其他类看的并访问

可访问有时也成为可见,它们可以互换使用。类的可访问性有两个级别:publicinternal

  • 标记为 public 的类可以被系统内任何程序集中的代码访问

    public class MyBaseClass
    {
        ...
    }
    
  • 标记为 internal 的类只能被自己所在的程序集内的类看到

    • 这是默认可访问级别,除非显式指定修饰符 public
    • 可以使用 internal 访问修饰符显式地声明一个类为内部的
    internal class MyBaseClass
    {
        ...
    }
    

程序集间的继承

C# 也允许从一个在不同的程序集内定义的基类来派生类。要从不同程序集中定义的基类派生类,需满足以下条件

  • 基类必须声明为 public
  • 必须在 Visual Studio 工程中的 Reference 节点中添加对包含该基类的程序集的引用

成员访问修饰符

  • 所有显式声明在类声明中的成员都是互相可见的,无论它们的访问性如何
  • 继承的成员不在类中显式声明,所以,继承的成员对派生类可以是可见的,也可以是不见的
  • 必须对每个成员指定访问级别。不指定,隐式访问级别为 private
  • 成员的可访问性不能比它的类高。如果一个类的可访问性限于它所在的程序集,那么类的成员在程序集外也不见,即便是 public 也不例外

访问成员的区域

一个类 B 能否访问另一个类 MyClass 的成员取决于该类的两个特征

  • 类 B 是否派生自 MyClass 类
  • 类 B 是否和 MyClass 类在同一个程序集

公有成员的可访问性

public 访问级别的限制最少。所有类,包括程序集内外都可以自由访问

私有成员的可访问性

private 的访问级别最严格,只能被自己的类成员访问,不能被其他类访问,包括继承它的类。然而 private 成员能被嵌套在它的类中的类成员访问

受保护成员的可访问性

同 C++,protected 访问级别如同 private,但允许派生自该类的类访问该成员,即使是从程序集外部继承该类

内部成员的可访问性

标记为 internal 的成员对程序集内部的所有类可见,但对程序集外部的类不可见

受保护的内部成员的可访问性

标记为 protected internal 的成员对所有继承自该类的类以及程序集内部的类可见,是 protectedinternal 的并集,而非交集

抽象成员

类似于 C++ 中的纯虚函数,抽象成员具有以下特征

  • 必须是一个函数成员
  • 必须用 abstract 修饰符标记
  • 不能实现代码块。抽象成员的代码用分号表示
  • 抽象成员只可以在抽象类中声明
  • 和虚成员一样,只有方法、属性、索引器和事件可以声明为抽象的
abstract class MyClass
{
    abstract public void PrintStuff(string s);
    abstract public int MyProperty { get; set; }
}
  • 尽管抽象成员必须在派生类中用相应成员覆写,但不能把 virtual 修饰符附加到 abstract 修饰符
  • 类似于虚成员,派生类中抽象成员的实现必须指定 override 修饰符

抽象类

同 C++ 抽象类的概念,设计为被继承的类,只能用作其他类的基类,不能创建抽象类的实例,使用 abstract 修饰符声明

  • 抽象类可以包含抽象成员或普通的非抽象成员
  • 抽象类自己可以派生自另一个抽象类
  • 任何派生自抽象类的类必须使用 override 关键字实现该类的所有抽象成员,除非该类自己也是抽象类

密封类

类似 C++ final 修饰符的效果,使用 sealed 修饰符标注的类只能被当作独立的类,不能作为基类被继承

sealed class MyClass
{
    ...
}

静态类

静态类中所有成员都是静态的。用于存放不受实例影响的数据和函数。常见用途是创建一个包含一组数学方法和值的数学库

  • 类本身必须标记为 static
  • 类的所有成员必须是静态的
  • 类可以有一个静态构造函数,但不能有实例构造函数,因为不能创建该类的实例
  • 静态类是隐式密封的,也就是说,不能继承静态类
static public class MyMath
{
    public static float PI = 3.14f;

    public static bool IsOdd(int x)
    {
        return x % 2 == 1;
    }

    public static int Times2(int x)
    {
        return x * 2;
    }
}

class Program
{
    static void Main()
    {
        int val = 3;
        Console.WriteLine("{0} is odd is {1}.", val, MyMath.IsOdd(val));
        Console.WriteLine($"{val} * 2 = {MyMath.Times2(val)}");
    }
}
// 3 is odd is True.
// 3 * 2 = 6

扩展方法

目前为止看到的方法都和声明它的类关联。扩展方法 特性扩展了这个边界,允许编写的方法和声明它的类之外的类关联。下面是一个非常有限的类

class MyData
{
    private double D1;
    private double D2;
    private double D3;

    public MyData(double d1, double d2, double d3)
    {
        D1 = d1;
        D2 = d2;
        D3 = d3;
    }

    public double Sum()
    {
        return D1 + D2 + D3;
    }
}

我们可以创建一个静态类,来扩展这个类的方法

static class ExtendMyData
{
    public static double Average(MyData data)
    {
        return data.Sum() / 3;
    }
}

class Program
{
    static void Main()
    {
        MyData md = new MyData(3, 4, 5);
        Console.WriteLine("Average: {0}", ExtendMyData.Average(md));
    }
}
// Average: 4

ExtendMyData.Average(md) 这种形式虽然实现了类似的需求,但我们更希望通过 md.Average() 来达成同样的效果

只需要对 Average 的声明做一个小小的改动,即可通过后者实例调用形式,就是在参数声明中的类型名前增加关键字 this

static class ExtendMyData
{
    public static double Average(this MyData data)
    {
       ...
    }
}

扩展方法的要求如下:

  • 声明扩展方法的类必须声明为 static
  • 扩展方法本身必须声明为 static
  • 扩展方法必须包含关键字 this 作为它的第一个参数类型,并在后面跟着它扩展的类的名称

表达式和运算符

表达式

表达式的概念和 C++ 没什么区别,可以作为操作数的结构有

  • 字面量
  • 常量
  • 变量
  • 方法调用
  • 元素访问器,如数组访问器和索引器
  • 其他表达式

字面量

Console.WriteLine($"{1024}"); // 整数字面量
Console.WriteLine($"{3.1416}"); // 双精度型字面量
Console.WriteLine($"{3.1416F}"); // 浮点型字面量
Console.WriteLine($"{true}"); // 布尔型字面量
Console.WriteLine($"{'A'}"); // 字符型字面量
Console.WriteLine($"{"Hi there"}"); // 字符串字面量

因为字面量是写进源代码的,所以必须在编译时可知。bool 和 C++ 一样,有两个字面量 true 和 false,都是小写的。引用类型有字面量 null,表示没有指向内存中的数据

整数字面量

基本同 C++,通过不同的后缀来解释为不同的整数类型,不区分大小写,没有后缀的会自动解释为不丢失数据的相应类型中最小的类型

236   // 整型
236L  // 长整型
236U  // 无符号整形
236UL //无符号长整型

除此之外和 C++ 一样,还可以用 0x 或 0X 开始表示十六进制,0b 或 0B 表示二进制,不过没有八进制的字面量

数字字面量可以插入 _ 当分隔符,方便阅读

实数字面量

基本同 C++,组成部分如下

  • 十进制数字
  • 一个可选的小数点
  • 一个可选的指数部分
  • 一个可选的后缀

无后缀时,默认为 doubleF 表示 floatD 表示 doubleM 表示 decimal,同样不区分大小写

字符字面量

基本和 C++ 一致,同样的转义字符,不多赘述

字符串字面量

常规字符串和 C++ 相同,不多赘述。

C# 还有一个逐字字符串,以 @ 为前缀,逐字字符串中的转义字符不会被转义,会原样输出,除了双引号,两个双引号解释为单个双引号

Console.WriteLine("Var1\t5");
Console.WriteLine(@"Var1\t5");
Console.WriteLine("It started, \"Four score and seven... \".");
// Console.WriteLine(@"It started, \"Four score and seven... \"."); // 会报错 \" 不会被转义
Console.WriteLine(@"It started, ""Four score and seven... "".");
// Var1    5
// Var1\t5
// It started, "Four score and seven... ".
// It started, "Four score and seven... ".

求值顺序

求值优先级

下面是微软官方最新的运算符优先级表

运算符 类别或名称
x.y、f(x)、a[i]、x?.y、x?[y]、x++、x--、x!、new、typeof、checked、unchecked、default、nameof、delegate、sizeof、stackalloc、x->y 主要
+x、-x、x、~x、++x、--x、^x、(T)x、await、&&x、*x、true 和 false 一元
x..y 范围
switch、with switchwith 表达式
x * y、x / y、x % y 乘法
x + y、x – y 加法
x << y、x >> y Shift
x < y、x > y、x <= y、x >= y、is、as 关系和类型测试
x == y、x != y 相等
x & y 布尔逻辑 AND 或按位逻辑 AND
x ^ y 布尔逻辑 XOR或按位逻辑 XOR
x| y 布尔逻辑 OR 或按位逻辑 OR
x && y 条件“与”
x|| y 条件“或”
x ?? y Null 合并运算符
c ? t : f 条件运算符
x = y、x += y、x -= y、x *= y、x /= y、x %= y、x &= y、x|= y、x ^= y、x <<= y、x >>= y、x ??= y、=> 赋值和 lambda 声明

结合性

当连续的运算符有相同的优先级时,求值顺序由结合性决定

  • 左结合运算符从左至右求值
  • 友结合运算符从右至左求值
  • 除赋值运算符外,其他二元运算符都是左结合的
  • 赋值运算符和条件运算符是右结合的

简单算术运算符和求余运算符

加减乘除取余和 C++ 没什么区别

关系比较运算符和相等比较运算符

比较运算符同 C++,值得一提的是,C# 中数字不具有布尔意义,也就是说整型之类的变量也无法转换成 bool 类型if(x) 之类的用法是错误的

对于引用类型的相等性,类似于 C++ 指针比较相等,只比较引用,只要指向同一个内存中的对象,就认为是相等的,也就是浅比较。string 类型除外,需要两个字符串完全相同,是深比较。

除此之外,委托也是引用类型,也是深比较,如果两个委托都是 null,或两者的调用列表中有相同数目的成员,且调用列表相匹配,那么比较返回 true

比较数值表达式时,将比较类型和值。比较 enum 类型时,将比较操作数的实际值

递增和递减运算符

基本同 C++ 自增和自减运算符

条件逻辑运算符

基本同 C++,注意短路效果,前一个表达式已经能确定整个条件逻辑表达式时,后一个表达式就不会计算了

逻辑运算符和移位运算符

基本同 C++ 的按位运算符和移位运算符

赋值运算符

基本同 C++ 的赋值运算符

可以放在赋值运算符左边的对象类型如下

  • 变量(局部变量、字段、参数)
  • 属性
  • 索引器
  • 事件

条件运算符

基本同 C++ 三目运算符,不过有一点不同,C# 的三目运算表达式不能作为单独一行语句,也就是说不能当成纯粹的控制语句来用,必须用赋值语句之类的来接住三目运算后的表达式的值。

一元算术运算符

基本同 C++,取正基本没有作用和主要是取负

用户自定义类型转换

C# 提供隐式和显式的类型转换,形式类似 C++ 的类型转换运算符重载,不同点在于其他类型转换为类不能通过有参构造函数来完成,只能通过运算符重载。通过 implicitexplicit 关键字来声明,下面是隐式版本,如果希望只能显式类型转换就把 implicit 换成 explicit

class LimitedInt
{
    private int _theValue = 0;

    public int TheValue
    {
        get { return _theValue; }
        set { _theValue = value; }
    }


    public static implicit operator int(LimitedInt li) // 括号里是源类型,括号外是目标类型
    {
        return li.TheValue;
    }

    public static implicit operator LimitedInt(int value)
    {
        LimitedInt li = new LimitedInt();
        li.TheValue = value;
        return li;
    }
}

C# 的强制类型转换运算符基本同 C 语言风格的强制类型转换

运算符重载

C# 的运算符重载更接近 C++ 友元函数形式的运算符重载

  • 运算符重载只能用于类和结构
  • 为类或结构重载一个运算符 x,可以声明一个名称为 operator x 的方法并实现它的行为
    • 一元运算符的重载方法带一个单独的 class 或 struct 类型的参数
    • 二元运算符的重载方法带两个参数,其中只有有一个必须是 class 或 struct 类型
  • 必须声明为 publicstatic
  • 运算符必须是要操作的类或结构的成员
class LimitedInt
{
    const int MaxValue = 100;
    const int MinValue = 0;

    private int _theValue = 0;

    public int TheValue
    {
        get { return _theValue; }
        set
        {
            if (value < MinValue)
                _theValue = 0;
            else
                _theValue = value > MaxValue ? MaxValue : value;
        }
    }

    public static LimitedInt operator -(LimitedInt x)
    {
        // 在这个奇怪的类中,取一个值的负数等于 0
        LimitedInt li = new LimitedInt();
        li.TheValue = 0;
        return li;
    }

    public static LimitedInt operator -(LimitedInt x, LimitedInt y)
    {
        LimitedInt li = new LimitedInt();
        li.TheValue = x.TheValue - y.TheValue;
        return li;
    }

    public static LimitedInt operator +(LimitedInt x, double y)
    {
        LimitedInt li = new LimitedInt();
        li.TheValue = x.TheValue + (int)y;
        return li;
    }
}

class Program
{
    static void Main()
    {
        LimitedInt li1 = new LimitedInt();
        LimitedInt li2 = new LimitedInt();
        LimitedInt li3 = new LimitedInt();
        li1.TheValue = 10;
        li2.TheValue = 26;
        Console.WriteLine($"li1: {li1.TheValue}, li2: {li2.TheValue}");
        li3 = -li1;
        Console.WriteLine($"-{li1.TheValue} = {li3.TheValue}");
        li3 = li1 - li2;
        Console.WriteLine($"{li1.TheValue} - {li2.TheValue} = {li3.TheValue}");
        li3 = li2 - li1;
        Console.WriteLine($"{li2.TheValue} - {li1.TheValue} = {li3.TheValue}");
        li3 = li1 + 10.3;
        Console.WriteLine($"{li1.TheValue} + 10.3 = {li3.TheValue}");
    }
}
// li1: 10, li2: 26
// -10 = 0
// 10 - 26 = 0
// 26 - 10 = 16
// 10 + 10.3 = 20

运算符重载的限制

可重载的一元运算符:+、-、!、~、++、--、true、false

可重载的二元运算符:+、-、*、/、%、&、|、^、<<、>>、==、!=、>、<、>=、<=

基本和 C++ 可重载的运算符范围一致,主要是多了个 true 和 false

  • 运算符重载不能创建新的运算符
  • 不能改变运算符的语法
  • 不能重新定义运算符如何处理预定义类型
  • 不能改变运算符的优先级和结合性

C# 递增和递减运算符和 C++ 一样有前置和后置的区别,不过和 C++ 不同的是重载的只有一个,C# 实际上重载的只是单纯的自增和自减部分,前后置带来的拷贝和返回值的部分是由编译器自动处理的。

  • 当你的代码对对象执行前置操作时,会发生以下行为
    • 在对象上执行递增或递减代码
    • 返回对象
  • 当你的代码对对象执行后置操作时,会发生以下行为
    • 如果对象是值类型,则系统会复制该对象;如果对象是引用类型,则引用会被复制
    • 在对象上执行递增或递减代码
    • 返回保存的操作数

所以对于后置的操作,因为复制时,是浅拷贝,对于引用类型,会达不到期望的效果,下面是 struct 版本,struct 是值类型,因此是正常的

public struct MyType
{
    public int X;

    public MyType(int x)
    {
        X = x;
    }

    public static MyType operator ++(MyType m)
    {
        m.X++;
        return m;
    }
}

class Program
{
    static void Show(string message, MyType tv)
    {
        Console.WriteLine($"{message} {tv.X}");
    }

    static void Main()
    {
        MyType tv = new MyType(10);
        Console.WriteLine("Pre-increment");
        Show("Before   ", tv);
        Show("Returned ", ++tv);
        Show("After    ", tv);

        tv = new MyType(10);
        Console.WriteLine("Post-increment");
        Show("Before   ", tv);
        Show("Returned ", tv++);
        Show("After    ", tv);
    }
}
// Pre-increment
// Before    10
// Returned  11
// After     11
// Post-increment
// Before    10
// Returned  10 如果把 struct 换成 class,这里就是 11
// After     11

这里可以用 IDE 查看一下生成的低级别的 C# 代码,可以很清晰的看出编译器底层做了什么,我们重载运算符实际上重载的是这里的 op_Increment

  private static void Main()
  {
    MyType tv1 = new MyType(10);
    Console.WriteLine("Pre-increment");
    Program.Show("Before   ", tv1);
    MyType tv2;
    Program.Show("Returned ", tv2 = MyType.op_Increment(tv1));
    Program.Show("After    ", tv2);
    MyType tv3 = new MyType(10);
    Console.WriteLine("Post-increment");
    Program.Show("Before   ", tv3);
    MyType myType = tv3;
    MyType tv4 = MyType.op_Increment(myType);
    Program.Show("Returned ", myType);
    Program.Show("After    ", tv4);
  }

typeof 运算符

和C++ 的 typeof 不同,C#的 typeof 运算符返回作为其参数的任何类型的 System.Type 对象。对于任何已知类型,只有一个 System.Type 对象。你不能重载 typeof 运算符。注意 typeof 传入的是类型,而非变量

using System.Reflection; // 使用反射命名空间来全面利用检测类型信息的功能

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 (var f in fi)
            Console.WriteLine($"Field: {f.Name}");
        foreach (var m in mi)
            Console.WriteLine($"Method: {m.Name}");
    }
}
// Field: Field1
// Field: Field2
// Method: Method1
// Method: Method2
// Method: GetType
// Method: ToString
// Method: Equals
// Method: GetHashCode

上面打印出的 GetType 方法也会调用 typeof 运算符

class SomeClass
{
}

class Program
{
    static void Main()
    {
        SomeClass s = new SomeClass();
        Console.WriteLine($"Type S: {s.GetType().Name}");
    }
}
// Type S: SomeClass

nameof 运算符

返回一个表示传入的变量、类型或者成员的字符串

string var1 = "Local variable";
Console.WriteLine(nameof(var1));                // var1
Console.WriteLine(nameof(MyClass));             // MyClass
Console.WriteLine(nameof(MyClass.Field1));      // Field1
Console.WriteLine(nameof(MyClass.Property1));   // Property1
Console.WriteLine(nameof(MyClass.Method1));     // Method1
Console.WriteLine(nameof(MyStruct));            // MyStruct
Console.WriteLine(nameof(parameters1));         // parameters1
Console.WriteLine(nameof(System.Math));         // Math
Console.WriteLine(nameof(Console.WriteLine));   // WriteLine

语句

什么是语句

C# 中的语句和 C/C++ 中的非常类似。

语句主要由 3 种类型

  • 声明语句 声明类型或变量
  • 嵌入语句 执行动作或管理控制流
  • 标签语句 控制跳转

简单语句由一个表达式和后面跟着的分号组成

块是由一对大括号括起来的语句序列。括起来的语句可以包括

  • 声明语句
  • 嵌入语句
  • 标签语句
  • 嵌套块
    int x = 10;         // 简单声明
    int z;              // 简单声明

    {                   // 块
        int y = 20;     // 简单声明
        z = x + y;      // 嵌入语句
        top: y = 30;    // 标签语句
        ...
        {               // 嵌套块
            ...
        }               // 结束嵌套块
    }                   // 结束块
}                       // 结束外部块

空语句仅由一个分号组成。在语言的语法需要一条嵌入语句而程序逻辑又不需要任何动作时,可以把空语句用在任何地方

if(x<y)
    ;			// 空语句
else
    z = a+b;	// 简单语句

表达式语句

表达式返回值,但它们也由副作用。

  • 副作用是一种影响程序状态的行为
  • 对许多表达式求值,只是为了它们的副作用

比如最常见的赋值表达式,我们在表达式后面放置分号,来表示从表达式创建语句,此时这个赋值表达式的返回值会被丢弃,我们对这个表达式求值的目的,就是为了这个赋值运算的副作用;

控制流语句

条件执行和循环结构(除了 foreach)需要一个测试表达式或条件以决定程序应当继续执行。与 C/C++ 不同,测试表达式必须返回 bool 型值,数字在 C# 中没有布尔意义

if 语句和 if...else 语句

基本和 C++ 一样,除了不能在条件表达式处声明变量,声明语句不算表达式

while 循环和 do 循环

同上

for 循环

基本和 C++ 一样,C++里可以实现的 C# 基本都能实现

switch 语句

常规用法上基本同 C++,唯一需要注意的就是如果测试表达式和模式表达式都是整数类型,则使用 C# 的相等运算符,其他情况都是使用静态方法 Object.Equals(test, pattern) 进行比较。也就是说对于非整数类型,C# 使用深比较

以及 goto 语句不能和非常量的 switch 表达式一起使用

C# 的 switch 语句的 case模式表达式并不像 C++ 一样局限于常量表达式(C# 7.0 的新特性)

public abstract class Shape
{
}

public class Circle : Shape
{
    public double Radius { get; set; }
}

public class Square : Shape
{
    public double Side { get; set; }
}

public class Triangle : Shape
{
    public double Height { get; set; }
}

class Program
{
    static void Main()
    {
        var shapes = new List<Shape>
        {
            new Circle { Radius = 7 },
            new Square { Side = 5 },
            new Triangle { Height = 4 }
        };
        var nullSqure = (Square)null;
        shapes.Add(nullSqure);

        foreach (var shape in shapes)
        {
            switch (shape) // 判断类型或者 shape 变量的值
            {
                case Circle circle: // 等价于 if (shape is Circle)
                    Console.WriteLine($"This shape is a Circle of radius {circle.Radius}");
                    break;
                case Square square when square.Side > 10: // 仅仅匹配一部分 Square
                    Console.WriteLine($"This shape is a large Square of side {square.Side}");
                    break;
                case Square square:
                    Console.WriteLine($"This shape is a Square of side {square.Side}");
                    break;
                case Triangle triangle:
                    Console.WriteLine($"this shape is a Triangle of height {triangle.Height}");
                    break;
                // case Triangle triangle when triangle.Height < 5 // 编译错误,因为已经被上一个 case 处理了
                //     Console.WriteLine($"This shape is a small Triangle of height {triangle.Height}");
                //     break;
                case null:
                    Console.WriteLine("This shape could be a Square,Circle or Triangle");
                    break;
                default:
                    throw new ArgumentException(
                        message: "shape is not a recognized shape", paramName: nameof(shape));
            }
        }
    }
}

亦或者下面这种关系模式

DisplayMeasurement(-4);  // Output: Measured value is -4; too low.
DisplayMeasurement(5);  // Output: Measured value is 5.
DisplayMeasurement(30);  // Output: Measured value is 30; too high.
DisplayMeasurement(double.NaN);  // Output: Failed measurement.

void DisplayMeasurement(double measurement)
{
    switch (measurement)
    {
        case < 0.0:
            Console.WriteLine($"Measured value is {measurement}; too low.");
            break;

        case > 15.0:
            Console.WriteLine($"Measured value is {measurement}; too high.");
            break;

        case double.NaN:
            Console.WriteLine("Failed measurement.");
            break;

        default:
            Console.WriteLine($"Measured value is {measurement}.");
            break;
    }
}

分支标签

和 C/C++ 不同,每一个 switch 段,包括可选的 default 段,必须以一个跳转语句结尾(break、return、continue、goto、throw)。C# 不允许直接从一个分支到另一个分支,除非分支标签之间没有任何可执行语句

switch(x)
{
    case 1:			// 可接受的
    case 2:
    case 3:
        ...			// 如果 x 等于 1、2 或 3,则执行该代码
        break;
    case 5:
        y = x + 1;
    case 6:			// 因为没有 break,所以不可接受
       	...
}

值得一提的是虽然跳转语句是最常用的结束分支块的方法,但编译器足够聪明,只要某个结构满足“不穿过规则”,它是可以检测到一个分支块是否能到达下一个分支块的,下面这个例子中,因为 while(true) ,所以永远不会进入下一个分支块,因此下面的代码是合法的

int x = 5;
switch (x)
{
    case 5:
        while (true)
            DoStuff();
    default:
        throw new InvalidOperationException();
}

break 语句和 continue 语句

基本同 C++

标签语句

标签语句由一个标识符后面跟着一个冒号再跟着一条语句组成。形式如下

Identifier: Statement

  • 标签语句的执行会忽略标签,只执行 Statement 部分
  • 给语句增加一个标签允许控制从代码的其他部分转移到该语句
  • 标签语句只允许在块内部

标签

标签可以和重叠作用域内的局部变量或参数同名,但不能和其他标签标识符同名,且不能是关键字

标签语句的作用域

  • 它声明所在的块
  • 任何嵌套在该块内部的块

goto 语句

可以无条件的跳转到一个标签语句

int x = 0;
while (true)
{
    ++x;

    if (x < 5)
        Console.WriteLine($"x is {x}");
    else
        goto Label;
}

Label:
Console.WriteLine($"Label reached with x = {x}");

switch 语句内的 goto 语句

goto 语句可以把控制转移给 switch 语句内部相应命名的分支标签,但是,goto 标签只能用来引用编译时常量(就行 C# 7.0 之前一样)

goto case ConstantExpression;
goto default;
goto case PatternExpression; // 编译错误

using 语句

某些类型的非托管对象有数量限制或很耗费系统资源。在代码使用完它们后,尽快释放它们非常重要。using 语句有助于简化该过程并确保这些资源被适当处置

资源是指是实现了 System.IDisposable 接口的类或结构。

使用资源分为三个阶段

  • 分配资源
  • 使用资源
  • 处置资源

如果使用资源的那部分代码中产生意外的运行时错误,那么处置资源的代码可能得不到执行

需要注意的是 using 语句和using 指令是不同的

包装资源的使用

using 语句简洁的包装了资源的使用

有两种形式的 using 语句,第一种形式如下

  • 圆括号内的代码分配资源
  • Statement 是使用资源的代码
  • using 语句隐式产生处置该资源的代码

using (ResourceType Identifier = Expression) Statement

其本质上是自动生成了 try...finally的异常处理代码

using System;
using System.IO;

namespace UsingStatement
{
    class Program
    {
        static void Main()
        {
            using (TextWriter tw = File.CreateText("output.txt"))
            {
                tw.WriteLine("Hello, World!");
            }

            using (TextReader tr = File.OpenText("output.txt"))
            {
                string InputString;
                while ((InputString = tr.ReadLine()) != null)
                    Console.WriteLine(InputString);
            }
        }
    }
}

生成的低层次 C# 代码如下

TextWriter tw = (TextWriter) File.CreateText("output.txt");
try
{
  tw.WriteLine("Hello, World!");
}
finally
{
  if (tw != null)
    tw.Dispose();
}
TextReader tr = (TextReader) File.OpenText("output.txt");
try
{
  string InputString;
  while ((InputString = tr.ReadLine()) != null)
    Console.WriteLine(InputString);
}
finally
{
  if (tr != null)
    tr.Dispose();
}

多个资源和嵌套

static void Main()
{
    using (TextWriter tw1 = File.CreateText("output.txt"),
           tw2 = File.CreateText("output2.txt"))
    {
        tw1.WriteLine("Hello, World1!");
        tw2.WriteLine("Hello, World2!");
        using (TextWriter tw3 = File.CreateText("output3.txt"))
        {
            tw3.WriteLine("Hello, World3!");
        }
    }
}

同时声明多个资源和嵌套使用 using,最后都是生成嵌套的try…finally块

using 语句的另一种形式

就是在外面声明,在里面使用,并不推荐使用,不能防止在已经释放了资源后使用该资源

TextWriter tw = File.CreateText("output.txt");
using (tw)
	tw.WriteLine("Hello, World!");