第5章——方法
var关键字
能够从初始化语句的右边推断出类型,e.g.
var total = 15;
var mec = new MyExcellentClass();
使用var的一些条件
- 只能用于本地变量,不能用于成员变量;
- 只能在变量声明中包含初始化时使用;
- 一旦编译器推断出变量的类型,它就是固定且不能更改的;
嵌套块中的变量
不管嵌套级别如何,都不能在第一个名称的有效范围内声明另一个同名的本地变量。
本地常量
- 在声明时必须初始化;
- 声明后不能改变;
- 初始化值必须在编译期决定;
- 可以是null,但不能是对象的引用;
值参数
- 在栈中为形参分配空间;
- 将实参的值复制给形参;
引用参数
- 必须在方法的声明和调用中都使用ref修饰符;
- 实参必须是变量,在用作实参前必须被赋值;
- 不会为形参在栈上分配内存;
输出参数
- 必须在声明和调用中都使用out修饰符;
- 实参必须是变量;
- 在方法内部, 输出参数在能够被读取之前必须被赋值,所以没必要在方法调用之前为实参赋值;
- 在方法返回之前,方法内部贯彻的任何可能路径都必须为所有输出参数进行一次赋值;
参数数组
- 在一个参数列表中只能有一个参数数组;
- 如果有,它必须是列表中的最后一个;
- 由参数列表表示的所有参数都必须具有相同的类型;
- 声明时在数据类型前使用params修饰符;
- 声明时在数据类型后放置一组空的方括号;
void listInts(params int[] inVals)
- 调用时不需要修饰符;
- 如果没有对应的实参,编译器会创建一个有零个元素的数组;
命名参数
- 调用时,·形参的名字后面跟着冒号和实际的参数值或表达式;
c.Calc(c:2, a:4, b:3);
- 如果同时使用位置参数和命名参数,所有位置参数必须先列出;
默认参数
- 只要值类型的默认值在编译的时候可以确定,就可以使用值类型作为默认参数;
- 只有在默认值是null的时候,引用类型才可以作为默认参数使用;
- 所有必填参数必须在默认参数声明前声明,如果有params参数,必须在所有默认参数之后声明;
- 如果需要随意省略默认参数列表中的默认参数,而不是从列表的最后开始,那么必须使用默认参数的名字(命名参数)来消除赋值的歧义;
第6章——深入理解类
成员常量
- 没有自己的存储位置,只是在编译时被编译器替换;
- 不能声明为static;
- 即使没有类实例也可以通过类访问,并且对类的每个实例都是可见的;
属性
public int myValue
{
set
{
SetAccessCode
}
get
{
GetAccessCode
}
}
与成员变量类似:
- 是命名的类成员;
- 有类型;
- 可以被赋值和读取;
与成员变量有区别:
- 不为数据存储分配内存;
- 执行代码;
- set访问器为属性赋值;
- get访问器为属性获取值;
set访问器总是:
- 拥有一个独立的,隐式的值参,名称为value,与属性的类型相同;
- 拥有一个返回类型void;
get访问器总是:
- 没有参数;
- 拥有一个跟属性类型相同的返回类型;
只读和只写属性:
- 只有get访问器的属性称为只读属性;
- 只有set访问器的属性称为只写属性;
- 两个访问器至少有一个必须定义;
自动实现属性:
- 不声明后备字段,编译器根据属性的类型分配存储;
- 不能提供访问器的方法体,必须被简单地声明为分号。get担当简单的内存读,set担当简单的内存写;
- 除非通过访问器,否则不能访问后备字段。必须同时提供读写访问器;
class C1
{
public int MyValue
{
set;get;
}
}
静态属性:
- 不能访问类的实例成员,但可以被实例成员访问;
- 不管类是否有实例,它们都是存在的;
- 当从类的外部访问时,必须使用类名引用,而不是类实例;
静态构造函数
- 初始化静态成员变量;
- 只能有一个静态构造函数,而且不能带参数;
- 不能有访问修饰符;
- 不能显示调用,系统会自动调用,在类的任何实例被创建之前,在类的任何静态成员被引用之前;
对象初始化语句
new TypeName {FieldOrProp=InitExpr, FieldOrProp=InitExpr,...}
new TypeName(ArgList) {FieldOrProp=InitExpr, FieldOrProp=InitExpr,...}
允许你在创建新的对象实例时,设置字段和属性的值。
- 初始化的字段和属性必须是public;
- 初始化发生在构造函数执行之后;
readonly修饰符
成员变量可以用readonly修饰符声明,其作用类似于const,一旦被设定就不能改变。
可以在下列任意位置设置它的值;
- 字段声明语句;
- 类的任何构造函数,如果是static字段,必须在静态函数中设置;
readonly字段的值可以在运行时决定;
- 可以是实例字段,也可以是静态字段;
- 在内存中有存储位置;
索引器
string this [int index, ...]
{
set
{
SetAccessCode
}
get
{
GetAccessCode
}
}
- 不用分配内存来存储;
- 可以只有访问器,也可以两个都有;
- 索引器总是实例成员,因此不能声明为static;
- 可以重载,参数列表必须不同。
访问器的访问修饰符
class Person
{
public string name {get; private set;}
}
- 仅当既有get访问器又有set访问器时,其访问器才可以有修饰符;
- 虽然两个访问器都必须出现,但只能有一个有访问修饰符;
- 访问器的修饰符必须比成员的访问级别有更严格的限制性;
分部类
- 每个分部类声明必须标为partial class;
- 每个分部类的声明都含有一些类成员的声明;
- 类的分部类声明可以在同一个文件中也可以在不同文件中;
分部方法
- 返回类型必须是void;
- 不能包含访问修饰符,所以分部方法是private的;
- 参数列表不能包含out参数;
- 定义声明和实现声明中都必须包含partial修饰符,直接放在关键字void之前;
- 可以只有定义部分而没有实现部分。这种情况下,编译器把方法的声明和方法内部任何对方法的调用都移除。不能只有分部方法的实现部分而没有定义部分。
第7章——类和继承
所有类都继承自object类
- 一个类的基类只能有一个,即单继承;
屏蔽基类的成员
- 要屏蔽数据成员,需要声明一个新的相同类型的成员,并使用相同的名称;
- 要屏蔽函数成员,需要声明新的带有相同签名的函数成员,签名由名称和参数列表组成,不包括返回类型;
- 要让编译器知道你在故意屏蔽继承的成员,要使用new修饰符,否则会报警告;
- 也可以屏蔽静态成员;
基类访问
- 基类访问表达式由关键字base后面跟着一个点和成员的名称组成;
base.Field1
虚函数和覆写方法
- 派生类的方法和基类的方法有相同的签名和返回类型;
- 基类的方法使用virtual标注;
- 派生类的方法使用override标注;
- 覆写和被覆写的方法必须有相同的可访问性;
- 不能覆写static和非虚函数;
- 方法,属性和索引器,以及事件都可以被声明为virtual和override;
- 当基类引用调用覆写的方法时,调用被沿着派生层次上溯执行,一直到标记为override的方法的最高派生版本;
- 如果在更高的派生级别有该方法的其他声明,但没有被标记为override,那么它们不会被调用;
构造函数执行顺序
构造函数初始化语句
- 使用关键字base指明使用哪个基类的构造函数;
public MyDerivedClass(int x, string y):base(x, y)
- 使用关键字this指明使用当前类的哪个构造函数;
public MyClass(int x):this(x, "using default string")
类访问修饰符
- 标记为public的类可以被系统内任何程序集中的代码访问;
- 标记为internal的类只能被它自己所在的程序集内的类看到,这是默认的可访问级别;
成员访问修饰符
- public
- private,能够被嵌套在它的类中的类成员访问
- protected
- internal
- protected internal,对所有继承该类的类以及所有程序集内部的类可见,是protected和internal的并集
抽象类
- 不能创建抽象类的实例;
- 抽象类使用abstract修饰符声明;
- 抽象类可以包含抽象成员或普通成员;
- 抽象类自己可以派生自另一个抽象类;
- 任何派生自抽象类的类必须使用override关键字实现该类的所有抽象成员,除非派生类自己也是抽象类;
抽象成员
- 必须是函数成员;
- 必须用abstract修饰符标记;
- 不能有实现代码;
- 只能在抽象类中声明;
- 只有四个类型的成员可以声明为抽象:
- 方法
- 属性
- 事件
- 索引
- 不能附加virtual修饰符;
- 派生类中抽象成员的实现必须指定override修饰符;
abstract public void PrintStuff(string s);
abstract public int MyProperty
{
get;
set;
}
密封类
- 只用用作独立的类,不能被用作基类;
- 用sealed修饰符标注;
静态类
- 用static修饰符标注;
- 所有成员必须是静态的;
- 可以有一个静态构造函数,但不能有实例构造函数,不能创建该类的实例;
- 静态类是隐式密封的,也就是说,不能继承静态类;
扩展方法
- 声明扩展方法的类必须声明为static;
- 扩展方法本身必须声明为static;
- 扩展方法必须包含关键字this作为它的第一个参数类型,并在后面跟着它所扩展的类的名称;
namespace ExtensiionMethods
{
sealed class MyData
{
private double D1, D2, D3;
public double Sum(){return D1+D2+D3;}
}
static class ExtendMyData
{
public static double Average(this MyData md)
{
return md.Sum() / 3;
}
}
class Program
{
static void Main()
{
MyData md = new MyData();
md.Average();
}
}
}
第8章——表达式和运算符
用户定义的类型转换
public和static修饰符是必须的
隐式转换
public static implicit operator TargetType (SourceType Identifier) { ... return ObjectOfTargetType; }
显示转换
public static explicit operator TargetType (SourceType Identifier) { ... return ObjectOfTargetType; }
运算符重载
public和static修饰符是必须的
public static LimitedInt operator +(LimitedInt x, double y)
typeof运算符
System.Type t = typeof(SomeClass);
第9章——语句
do循环
do
...
while(...);
using语句
using管理的资源必须实现System.IDisposable接口
using (ResourceType Id1 = Expr1, Id2 = Expr2, ...) EmbeddedStatement
第10章——结构
结构与类的区别:
- 类是引用类型而结构是值类型;
- 结构是隐式密封的,所以是不能被派生;
- 结构可以有实例构造函数和静态构造函数,但不允许有析构函数;
- 编译器隐式地为每个结构提供一个无参数的构造函数,而且不能被删除或重定义,而类只有在没有其他构造函数声明时才会有默认构造函数;
- 字段初始化语句是不允许的;
- 所有结构都派生自System.ValueType;
第11章——枚举
设置底层类型
enum TrafficLight : ulong
{
...
}
位标志
enum CardDeckSettings : uint
{
SingleDeck = 0x01, //位0
LargePictures = 0x02, //位1
FancyNumbers = 0x04, //位2
Animation = 0x08 //位3
}
CardDeckSettings ops = CardDeckSettings.SingleDeck
| CardDeckSettings.FancyNumbers
| CardDeckSettings.Animation;
//判断标志字是否包含特定的位标志集
bool useFancyNumbers = ops.HasFlag(CardDeckSettings.FancyNumbers);
第12章——数组
数组
- 数组一旦创建,大小就固定了,C#不支持动态数组;
- 有两种类型多维数组:
- 矩形数组:某个维度的所有子数组有相同长度的多维数组;不管有多少维度,总是使用一组方括号
int x = myArray[4,6,1];
- 交错数组:每个子数组都是独立数组;可以有不同长度的子数组;为数组的每个维度使用一对方括号
myArray[2][7][4]
- 矩形数组:某个维度的所有子数组有相同长度的多维数组;不管有多少维度,总是使用一组方括号
- 数组是对象9(引用类型),数组实例是从System.Array继承的对象;
一维数组和矩形数组的声明
int[] array1; //一维数组
int[,] array2; //二维数组
int[,,] array3; //三维数组
- 可以使用任意多的秩说明符(逗号);
- 不能设置数组各个维度长度;
- 数组声明后,维度数就固定了,维度长度要等到实例化才能确定;
一维数组和矩形数组的实例化
int arr1 = new int[5];
int[,,] arr2 = new int[3,6,2];
一维数组和矩形数组的显示初始化
不用输入维度长度,编译器可以通过初始化值的个数来推断长度
int[,] arr = new int[,]{{1,2},{3,4},{5,6}};
交错数组
- 声明时每个维度都有一对独立的方括号;
- 不能在声明时初始化顶层数组之外的数组
int[][] arr = new int[3][4]//error
每个子数组要单独创建
int[][] arr = new int[3][]; arr[0] = new int[] {1,2,3}; arr[1] = new int[] {1,2}; arr[2] = new int[] {1,2,3,4,5};
子数组可以是矩形数组
int [][,] arr = new int[3][,]; arr[0] = new int[,]{{1,2},{3,4}}; ...
第13章——委托
delegate void MyDel(int value); //声明委托类型
class Program
{
void PrintLow(int value){...}
void PrintHigh(int value){...}
static void Main()
{
Program program = new Program();
MyDel del; //声明委托变量
del = new MyDel(program.PrintLow);
//del = program.PrintLow; 另一种简洁写法
del(10); //执行委托
}
}
组合委托
MyDel delA = myInstObj.Fun;
MyDel delB = SClass.Fun;
MyDel delC = delA + delB;
delA += delB;
从委托移除方法
delA -= SClass.Fun;
- 如果在调用列表中的方法有多个实例,-=运算符将从列表最后开始搜索,并且移除第一个与方法匹配的实例;
- 试图删除委托中不存在的方法没有效果;
- 试图调用空委托会抛出异常。可以通过把委托与null进行比较判断委托的调用列表是否为空。如果调用列表为空,则委托是null;
调用带有返回值的委托
- 调用列表中最后一个方法返回的值就是委托调用返回的值;
- 调用列表中所有其他方法的返回值都会被忽略;
调用带引用参数的委托
- 在调用委托列表中的下一个方法时,参数的新值会被传给下一个方法;
匿名方法
class Program
{
delegate int Del(int InParam);
static void main()
{
Del del = delegate(int x)
{
return x + 20;
}
}
}
- 当满足以下两个条件时可以省略匿名方法的参数列表:
- 委托的参数列表不包含任何out参数;
- 匿名方法不适用任何参数
- 如果委托类型声明指定最后一个参数为params类型的参数,匿名方法参数列表忽略params关键字;
- 匿名方法可以访问它们外围作用域的局部变量和环境;
第14章——事件
例子:
delegate void Handler(); //声明委托
class Increment
{
public event Handler CountedADozen; //创建事件并发布
public void DoCount()
{
for(int i = 0; i < 100; ++i)
if(i % 12 == 0 && CountedADozen != null)
CountedADozen(); //触发事件
}
}
class Dozens
{
public int DozensCount {get; private set;}
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount; //订阅事件
}
//事件处理程序
void IncrementDozensCount()
{
DozensCount++;
}
}
class Program
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozensCounter = new Dozens(incrementer);
incrementer.Docount();
}
}
移除事件
- 跟委托移除方法一样,用-=运算符;
- 如果一个处理程序向事件注册了多次,当移除处理程序时,只会移除列表中该处理程序的最后一个实例;
第15章——接口
声明接口
- 接口声明不能包含以下成员:
- 数据成员
- 静态成员
- 只能包含以下类型的非静态成员函数的声明:
- 方法
- 属性
- 事件
- 索引器
- 函数成员的声明不能包含任何实现代码;
- 接口名称习惯都以大写的I开始;
- 支持分部接口声明;
- 接口成员时隐式public的,不允许有任何访问修饰符;
- 只有类和结构才可以实现接口;
- 如果类从基类继承并实现了接口,基类列表中的基类名称必须放在所有接口之前;
- 可以实现任意数量的接口;
- 如果一个类实现了多个接口,其中一些接口有相同签名和返回类型的成员,那么类可以实现单个成员来满足所有包含重复成员的接口;
- 支持继承;
as运算符
- 如果类实现了接口,表达式返回指向接口的引用;
- 如果类没有实现接口,表达式返回null而不抛异常;
显示接口成员实现
class MyClass: IIfc1, IIfc2
{
void IIfc1.PrintOut(string s) {...}
void IIfc2.PrintOut(string s) {...}
}
- 只能通过指向接口的引用来访问,其他的类成员都不可以直接访问它们;
转换
溢出检测上下文
checked(表达式);
unchecked(表达式);
checked
{
...
}
unchecked
{
...
}
- 如果在转换时会产生溢出则抛出一个OverflowException异常;
装箱转换
- 装箱是一种隐式转换,它接受值类型的值,根据这个值在堆上创建一个完整的引用类型对象并返回对象引用;
拆箱转换
- 拆箱是显示转换;
is运算符
Expr is TargetType
如果Expr可以通过以下方式成功转换为目标类型,运算符返回true
- 引用转换
- 装箱转换
- 拆箱转换
as运算符
- 类似于强制转换运算符,只是不抛出异常,如果转换失败,返回null;
- 转换的目标类型必须是引用类型;
- 只能用于引用转换和装箱转换;
第17章——泛型
类型参数的约束
where TypeParam: constraint, constraint, ...
e.g.
class MyClass<T1, T2, T3>
where T2:Customer
where T3:IComparable
{
...
}
共有五种类型的约束:
- 类名:只有这个类型的类或从它继承的类才能用作类型实参;
- class:任何引用类型,包含类,数组,委托和接口都可以用作类型实参;
- struct:任何值类型都可以用作类型实参;
- 接口名:只有这个接口或实现这个接口的类型才可以用作类型实参;
- new():任何带有无参数公告构造函数的类型都可以用作类型实参;这叫做构造函数约束;
where子句可以以任何次序列出,然而,where子句种的约束必须有特定的顺序
- 最多只能有一个主约束,如果有必须放在第一位;
- 可以有任意多的接口名约束;
- 如果存在构造函数约束,则必须放在最后;
泛型方法
public void PrintData<S, T>(S p, T t) where S:person
{
...
}
第18章——枚举器和迭代器
枚举器和可枚举类型
- 获取一个对象枚举器的方法是调用对象的GetEnumerator方法。实现GetEnumerator方法的类型叫做可枚举类型;
- foreach会调用GeetEnumerator方法获取对象的枚举器,从枚举器中请求每一项并且把它作为迭代变量,代码可以读取该变量但不可以改变;
- IEnumerator接口包含三个函数成员:Current,MoveNext,Reset;
- IEnumerable接口包含一个函数成员:GetEnumerator。它返回对象的枚举器;
迭代器
//产生枚举器的迭代器
public IEnumerator<string> IteratorMethod()
{
...
yield return ...;
}
//产生可枚举类型的迭代器
public IEnumerable<string> IteratorMethod()
{
...
yield return ...;
}
第21章——命名空间和程序集
using指令
- 必须放在源文件的顶端,在任何类型声明之前;
- 它们应该于当前源文件中的所有命名空间;
可以给命名空间或命名空间内的类型起别名;
using Syst = System; using SC = System.Console;
第23章——预处理指令
- 预处理指令必须和C#代码在不同的行;
- 不需要以分号结尾;
- 每行必须以#字符开始;
- 允许行尾注释;
- 预处理指令所在的行不允许分隔符注释;
- #define和#undef只能用在源文件的第一行,在C#代码开始后就不能再使用;
区域指令
- 被放置再希望标注的代码段之上;
- 用指令后的可选字符串文本作为其名字;
- 在之后的代码中必须由#endregion指令终止;
- 可以嵌套;
- 用visual studio允许隐藏或显示区域;
pragma warning指令
–
#pragma warning disable 615,414 //关闭部分警告
#pragma warning restore 618 //重新开启部分警告
#pragma warning disable //关闭所有警告
#pragma warning restore //重新开启所有警告
第24章——反射和特性
Obsolete特性
class Program
{
//[Obsolete("Use method SuperPrintOut", true)] //true表示被标记为错误而不仅是警告
[Obsolete("Use method SuperPrintOut")]
static void PrintOut(string str)
{
...
}
static void Main(string[] args)
{
PrintOut("xxx");
}
}
Conditional特性
#define DoTrace
usig System;
class Program
{
[Conditional("DoTrace")]
static void TraceMessage(string str)
{
Console.WriteLine(str);
}
static void Main()
{
TraceMessage("Start of Main");
Console.WriteLine("Doing work in Main");
TraceMessage("End of Main");
}
}
如果DoTrace被定义,那么编译器会包含所有对TraceMessage的调用代码;否则,不会调用。
调用者信息特性
using System;
using System.Runtime.CompilerServices;
public static class Program
{
public static void MyTrace(string message,
[CallerFilePath] strig filename = "",
[CallerLineNumber] int lineNumber = 0,
[CallerMemberName] string callingMember = "")
{
...
}
public static void Main()
{
MyTrace("Simple message");
}
}
DebuggerStepThrough特性
- 告诉编译器在执行代码时不要进入该方法调试;
- 位于Sytem.Diagnostics空间;
- 可用于类,结构,构造函数,方法或访问器;
多个特性
- 多个特性可以以下面两种形式列出:
- 分成两个独立的特性片段相互叠在一起;
- 单个特性片段,特性之间用逗号分隔;
- 可以以任何次序;