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
方法或非虚方法 - 方法、属性和索引器,以及另一种成员类型——事件,都可以被声明为
virtual
和override
覆写标记为 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; } }
类访问修饰符
类可以被系统中的其他类看的并访问
可访问有时也成为可见,它们可以互换使用。类的可访问性有两个级别:public
和 internal
-
标记为
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
的成员对所有继承自该类的类以及程序集内部的类可见,是 protected
和 internal
的并集,而非交集
抽象成员
类似于 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++,组成部分如下
- 十进制数字
- 一个可选的小数点
- 一个可选的指数部分
- 一个可选的后缀
无后缀时,默认为 double
,F
表示 float
,D
表示 double
,M
表示 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 | switch 和 with 表达式 |
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++ 的类型转换运算符重载,不同点在于其他类型转换为类不能通过有参构造函数来完成,只能通过运算符重载。通过 implicit
和 explicit
关键字来声明,下面是隐式版本,如果希望只能显式类型转换就把 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 类型
- 必须声明为
public
和static
- 运算符必须是要操作的类或结构的成员
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!");