// 以下为学习C#期间的笔记,暂不勘误,留作复习用(内容均来自C# in a nutshell 与 msdn)
C#的特征,封装、继承、多态(所有的面向对象语言所共有的);
特别的C#不只有类可以继承,还有interface可以继承(同类类似,但是仅描述类成员,多继承常用),默认情况下C#不支持类的多继承;
C#中的函数由属性以及事件两部分构成,属性用来描述状态,事件则用于处理状态变化,methods are only one kind of function member;
C#是类型安全的——强类型,任何跨类型的赋值都会遭到警告;
C#自动管理内存;
C#比较好的非微软运行环境是Mono Project(Unity3D也在用这个),C#在运行时需要依赖一个运行环境(runtime)以期其提供内存管理异常处理等功能,即便C#自身是独立的(比较好的例子是LinqPad里,仅用Statement段就可以运行C#),C#在设计上与微软的CLR(Common Language Runtime)是趋同的,数据类型相近,设计理念类似,但是细节写法上与原生的C#不同,具体需要参考错误信息提示;
C#允许递归;
C#也有值传递与引用传递(指针一类),但是与C++不同,C#辨别传递方式依据的是变量以及关键字而不是修饰符(&... *...);
C#也有函数默认参数(该性质与C++相同,声明默认参数从右向左不可间隔,调用参数从左向右不可间隔);
C#函数调用时可以用“参数名:参数”的方式指明输入参数——全指明时可以不按次序;
C#的操作符跟C++的差不多+=、==、=、*=、<<=等,()的优先级也为最高。特别的对于Null有??、?.、?[]、??=,param1??param2,表示当param1不为null时使用param2的值,否则为param1,运算方向从右向左x??y??z等于x??(y??z);
C#的重载同C++;
C#也能够通过私有构造函数形成单件;
C#的实例可以直接在创建时就进行初始化classX instance = new classX{param1 = ..., param2 = ..., param3 = ..., ...};
C#的向上转型不需要(BaseTypeClass)param1,向下转型需要(SubTypeClass)param1——因其可能转换失败;
C#中子类需要定义自己的构造函数,这点同C++一样,如果使用base(),则自动调用基类构造方法,如public Subclass (int x) : base (x) { };
基础类型定义:C#的基础数据类型都来自于同一个基类,基础类型有共有的方法,例如使用ToString()可以将其他基础类型转换为字符串(自定义类型自行重载)
(Nbit代表可以存储的长度,NByte代表内存占用的大小——且与系统有关)
sizeof(sbyte) 1
sizeof(byte) 1
sizeof(short) 2
sizeof(ushort) 2
sizeof(int) 4
sizeof(uint) 4
sizeof(long) 8
sizeof(ulong) 8
sizeof(char) 2
sizeof(float) 4
sizeof(double) 8
sizeof(decimal) 16
sizeof(bool) 1
short 16bit 整型(Nbit表示存储最大值为2的N次幂)
short 16bit Z整型
sbyte 8bit 整型
byte 8bit Z整型
int 32bit 整型
uint 32bit Z整型
long 64bit 整型
ulong 64bit Z整型
float 32bit 浮点数
double 64bit 浮点数
调取该类型的边界值使用xxx.MinValue及xxx.MaxValue;
另外,整型再做加减法时,溢出值会自动循环;
使用checked关键字检查是否溢出(可用于表达式可用于代码段);
使用unchecked关键字避免编译器对指定溢出的代码段报错;
byte、sbyte、short以及ushort用于表示8与16进制,在使用时会被转化为更大的类型(因为本身没有专门的运算函数),其相关变量进行运算时需要进行类型转换;
浮点数有Infinite(1.0/0.0)、-Infinite(-1.0/0.0)、以及NaN(0.0/0.0),但是两个NaN值不相等,即便它们是同一类对象;
同其他语言一样,浮点数本身有误差值-1.490116E-09左右,使用decimal则没有这个问题,但是对于无法除尽的数,float、double、decimal都无法正常表示(存在位长限制的精度极限);
C#的数值表示可以使用二进制(0b C#7.0及更新版本可用)十进制、八进制(C++中倒是可以)、十六进制(0x)、科学计数法(1.23E06),另外,使用Convert.ToInt32(string value, int fromBase)转换其他进制的数为十进制,使用Convert.ToString(int value, int toBase)将十进制转化为其他进制。
数字类型后缀(不区分大小写)
F = float
D = double
M = decimal
U = uint
L = long
UL = ulong
char 字符
string 字符串
字符可以转换成数字,int与ushort可以直接转换,而其他整形需要显示转换。另外,格式符‘\\‘、‘\n‘等可以用char存储;
字符串在需要使用格式符时可以选择使用@"..."的方式,直接写出字符串,避免使用过于繁琐的格式符。例如:
string escaped = "A\tB\\";
string verbatim = @"A B\";
字符串还可以进行插值,$"...",例如:
string text = $"A square has {x} sides"; // x可以是变量
@"..."与$"..."可以连用;
队列声明使用new关键字(产生的队列都为Null),或者直接使用静态定长。
int[5] b = {0, 1, 2, 3, 4};
int[] a = new int[1000];
char[] viewlist = new char[5]; // {0, 0, 0, 0, 0}
Point[] points = new Point[100]; // Point为自定义类型,当为自定义类型时,成员对象未初始化时为Null
声明矩阵时,
int [][] matrix = new int [N][]; // 行列都为N,需要逐项初始化
int[][] matrix2 = new int[][] // 手动设置行列
{
new int[] {0,1,2},
new int[] {3,4,5},
new int[] {6,7,8,9}
};
int[][] jaggedMatrix = // new关键字可以省略
{
new int[] {0,1,2},
new int[] {3,4,5},
new int[] {6,7,8}
};
var vowels = new[] {‘a‘,‘e‘,‘i‘,‘o‘,‘u‘}; // 隐式转换字符型数组vowels.GetType()为char[]
var x = new[] { 1, 10000000000 }; // 隐式转换long型数组x.GetType()为Int32[]/Int64[]——根据系统位数来的
值类型、引用类型参数(二者区别同C/C++)。
值类型:struct
内置值类型:所有的数(int、uint、byte、sbyte、long等)、bool、char
引用类型:class、interface、delegate
内置引用类型:dynamic、object、string
ref:指明传递参数为引用(默认为值传递)
out:用于函数参数传出修饰符
特别的:
类定义中的field——与C++的类成员一样;property也是类成员但是有get set方法,例如:
decimal realPrice;
public decimal price
{
get { return realPrice; } set { realPrice= value; } // 不写set部分则为只读
}
// public decimal price => realPrice; // 不写set时的等价写法(expression-bodied)
// public decimal price { get; set; } // 自动属性
// public decimal price { get; set; } = 123.4M // 带有初始值的自动属性
C#中的常量(const关键字定义)在类中声明时,会"baked into the calling site"而不是消耗编译时间,而定义在全局(scope-method)时,会消耗编译时间(如果存在任何运算)
static修饰的静态构造函数,一个类型只初始化一次——多个实例也只是一次。(C++里木得这种用法)
nameof的用法之一(可能也就抛出异常——报错能用用了)
public string Name { get => name; set => name = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null"); }
Boxing与Unboxing,前者由值类型->Object或者由该类型实现的接口类型,后者相反(且显式转换的类型必须对应),例如,
int x = 9;
object obj = x; // Box the int, implicitly
int y = (int)obj; // Unbox the int
这里暗含了C#万物皆可obj的思想,但是Boxing与Unboxing的开销并不小(为Object提供内存空间,Cast也是需要消耗资源的)
typeof(C++里也有这货),获取类型——检验运行时的类型是否吻合,is也是这个效果,但是在应用继承的时候不同,typeof与GetType都是返回的当前实例类型,GetType是针对System.Type的;
C#中struct相对于class的几点不同(以下几点如果是一个类,则都可以):
public struct Point
{
int x = 1; // Illegal: cannot initialize field
int y;
public Point() { } // Illegal: cannot have parameterless constructor
public Point (int x) { this.x = x; } // Illegal: must assign field y
}
关于C#的类访问权限,除了C++孰知的public(公交车)、private(胳膊肘一直朝自己拐)、protected(宗族观念者)之外,还有internal(当前组件可访问)、private protected(仅该类的实例可访问而且必须在当前组件内)以及internal protected(本类的实例及子类可访问,但是范围仅限当前组件)。
for,while等基础语句:
if、else-if、switch、while、do-while、for同C++;
foreach(var c in param1)
{
// todo: manipulate c
}
break用于跳出至上一层,continue用于略过本次循环,return跳出循环结构——函数返回值,goto至某一个字段,同C++;
关键字(及部分可调函数):
class 类(面向对象概念,不再赘述);
struct 结构体,可用于定义自定义类型(用class也可以,但是未定义复制构造函数时,使用struct定义数据默认为值复制,使用class定义数据默认为地址复制——引用);
new 用于声明类型实例(并分配内存),在子类基类成员同名时用于子类成员以显式隐藏基类成员,基类成员仍可显式调用;
null 特定值,通常情况下值类型不为null(只有类似class这样的引用类型的实例可为null,很多预定义类型默认不可为null的),因此同null的比较只有引用类型才是合法的(跟其他语言一样——只不过引用类型的定义范围略有不同);
var 用于声明变量,让编辑器自行判断变量类型,具备某一类型的数据之后不能再用其他类型对其进行转换;
static (类似C++却限制更大,但是用于基础变量(field)时,自动初始化为默认值,且先于构造函数),static关键字的属性变量不能被this引用到,常量及类型定义默认为静态,作为类修饰词时,类的成员也必须是静态的;
readonly 只读,可用于struct,可用于值类型,可用于引用类型(依旧能够改变引用内容,但是引用不可变),readonly修饰变量必须在构造函数之前,可以在定义时初始化,也可以在构造函数內初始化;
partial 用于提供函数hook以便让类的设计者决定是否执行(且同一个类的声明与实现可以在两个文件内),partial修时候不能再用访问权限关键字进行修饰(默认为private);
is 类实例的类型判断、函数返回值的常量判断、类实例的null判断(用==得把null放左侧的那种),格式为expr is type varname;
as 尝试将一个类型转换成另一个类型,不成功返回null——不报异常,限定类型为reference, nullable, boxing, and unboxing,用户自定义类型需要用cast,格式为expr as type // expr is type ? (type)(expr) : (type)null;
this 在构造函数中,指当前的类自身,或者作为其他方法的传入参数,
或者用作索引,
public int this[int param] { get { return array[param]; } set { array[param] = value; } }
或者用于extension methods第一个参数的修饰符;
virtual 基类中修饰(method, property, indexer, or event declaration),表示为可重载;
override 子类中修饰被重载的部分(基类中的部分必须为virtual, abstract, or override),与virtual配合使用——需要注意的是virtual过的部分在子类中不是必须被重载的,override无法修改virtual部分的访问等级,override后的子类对象在向上转型后依旧为子类实现;
abstract 不完整的类,需要由非抽象类实现其函数,无法产生实例,不能同sealed同时使用(sealed表示不能被继承),子类需要实现所有方法,修饰方法时不能同static跟virtual联用,但是,abstract类可以作为virtual类的子类,这样会阻止后续子类访问virtual本来的方法成员;
sealed 用于修饰属性、方法、类以使属性、方法不能被后续的子类override,类无法被再继承。
interface 仅包含signatures of methods, properties, events or indexers,任何继承接口的类或者结构体都需要对其成员进行实现,实现接口默认为隐式,使用显式实现,既xxxInterface.xxxMethod(),以避免由不同接口的相同成员名造成的冲突——但这样调用该成员仅能通过相应接口的实例而不是类的实例,另外,同时继承类与接口时,必须让类的部分优先;
enum 枚举,其默认的值类型为int,允许的类型为byte,short,int,long等integral numeric type,使用举例,enmu Day:byte{...},可定义当前文件的全局枚举,也可以定义某个类的内部枚举,枚举可进行赋值(未赋值的部分遵循递增规则),赋值也可使用其他枚举,枚举型转换为任何非本枚举型的类型都需要进行显式转换。枚举型在作为全局标记量使用时,需要注释以明确说明各个成员的作用——以避免新变量加入造成的错误难于排查,一般用于switch语句内。枚举型的[flags]参见System.FlagsAttribute;
基础可用类:
(以下类均不能使用using System.XXX;——毕竟是类不是库)
System.Random 用来获取随机数;
System.Math 各种数学可用函数sqrt sin abs log log10等;
System.Object C#宇宙内,类型的万物之源,任何的类型都能隐式的转换成Object;
System.Type .Net各种类型的集合;
System.FlagsAttribute 针对枚举,标明当前枚举为标记,最为显著的区别是,将某个枚举型内没有对应值的数转换为枚举型,未使用[flags]时,以原数表示,使用[flags]时以已定义的枚举值拼接表示(此时的枚举作为标记可以使用|=、^=、|、^跟&进行旗帜增减——但是枚举自身的定义值只能为2的N次,以确保不重叠);
System.MulticastDelegate 任何委托类型的共有基类(System.Delegate的子类);
System.Func & System.Action 已定义好的泛型委托,前者带返回值,后者不带;
System.EventArgs 预定义的框架类,用于转让事件信息;
System.EventHandler 处理事件的委托,符合.Net定义(public delegate void EventHandler(object sender, EventArgs e);),在事件没有额外信息时使用;
基础可用库:
System 系统工具集;
System.Collections.Generic 常用的泛型合集;
泛型基础:
使用泛型可以减少调用开销、避免boxing与unboxing,主要的目的同C++的STL是一样的,相同功能下减少不同数据类型造成的类型识别代码编写——多类型重载,便于代码重用。可以自己编写也可使用System.Collections.Generic内现有的类。
使用泛型时,需要在类前标明未知类型,例如,class Node<T,T1,T2,T3>{ ... },这同C++一致。其中T仅仅是个“约定俗成”的写法,写作其他内容也是可以的,目的是让T替换掉原本应该使用object类(在使用boxing机制时)或者其他具体类型的代码段。使用泛型的类声明实例时也需要标明未知类型,例如,Node<T> firstNode。
在需要使用特定化实例的时候,替换为特定类型及可。例如,Node<int> intNodes = new Node<int>(),鉴于泛型的类是在初始化的时候才确定类型的(这一特性是泛型的核心),故此对象初始化后,任何跟这个对象有关的类的方法及部分成员都跟对象类型一致,因而数据安全性也得到了保障——不会允许输入预想之外的数据类型。
泛型的优点显而易见(安全性、避免装箱消耗,因为是编译时决定类型——生成对应类型的代码副本,实际产出的二进制文件跟每个类型都做一个特定版本产出的差异不大,但是代码量骤减),缺点就是难写,代码一旦复杂起来编写跟维护都很费劲。
default 获取该类型的默认值,例如,泛型中可以使用defualt(T)获取T所代表的类型的默认值。
where 泛型类型约束,当代码尝试使用某个约束不允许的类型进行实例化时,产生编译错误。例如,class GenericClass<T> where T : class, SomeInterfaces, new() { ... },反过来讲,当泛型部分需要调用部分功能的时候,使用where不仅仅是对其进行限定,可以算作是反向的对未知类型的可调用方法进行了扩展。
T : class | 参数类型必须为引用类型,包括任何类、接口、委托或数组类型 |
T : struct | 参数类型必须为值类型,可以指定除Nullable以外的任何类型 |
T : <基类名> | 参数必须为该类或者该类的子类 |
T : <接口名> | 参数必须为该接口或者接口的实现接口,可指定多个接口约束,接口约束也可以是泛型的 |
T : new() | 参数类型必须具有无参数的公共构造函数。当同其他约束一起使用时,new约束必须为最后一个 |
T : U | 为T提供的参数必须是为U提供的参数,或者是派生自为U提供的参数。称之为裸类型约束 |
约束一般放在实际继承之后——where之前,例如,class GenericClass<T> : BaseClass where T : struct { ... };
当接口约束同类约束同时使用时,类名在前——跟继承时优先次序一样;
多个参数存在约束时,class GenericClass<T,U> where T : class where U : new() { ... };
一般情况下,子类继承泛型基类可以特定化类型,例如,
class CertainClass : GenericClass<int> { ... }
子类也可以是泛型的,例如,
class GenericSubClass<R> : GenericClass<R> { ... }
class GenericSubClass<R, V> : GenericClass<R> { ... } // 引入新的未知类型
而当基类有约束时,例如,
class GenericClass<T,U> where T : class { ... }
当子类为泛型时,则需要重复基类的所有约束,例如,
class GenericSubClass<T,U> : GenericClass<T,U> where T : class { ... };
泛型方法既可以放在泛型类中,也可以放在非泛型类中。泛型方法可以重载、复写,且不需要复写约束。
泛型中的static变量都是单独对应的,既每一个类型都有自己对应的静态变量,前一个类型对静态变量的操作不会影响另一个类型——成因是编译器要生成对应类型的副本。
当需要在泛型中加入特定类型的转化时,不能使用显式转换,因为T是未知类型,可行的是使用
T arg;
someType param1 = arg as someType; // as关键字,转换不可行时返回null
if(null != param1) { return param1; }
return null;
或者用一种再通俗点的方式
T arg;
if(arg is someType) { return (someType)(object)arg; } // 利用装箱拆箱
return null;
当需要泛型方法返回特定值时可以使用以上功能(虽然直接使用降低了数据安全性);
泛型参数为类时,无法正常进行类之见的上下转型,无法进行逆变协变。假设C、B分别继承自A,有一个Stack<T>装填类,Stack<A> param1= new Stack<B>()这样的语句在编译时是要出错的,解决方法是在存在类似情况的地方尽量使用泛型的Stack<T>配合使用where语句——尽可能的让编译器在编译时产生对应的副本(多类型杂糅在一起的情况依旧要避免),或者利用接口的"实现与接口之见的共通性",让Stack<T>继承自Interface<T>,然后,使用Interface<A> param1 = new Stack<B>()这样的语句——符合语法;
Delegate委托:
委托实例用于调用符合定义的函数。例如,
delegate in Transformer(int x);
static int Square(int x) => x*x;
Transformer t = Square; // 实际是Transformer t = new Transformer(Square);
int answer = t(3); // 调用为Square(3),如果存在重载,则会根据正确的重载函数进行调用。实际是t.Invoke(3);
delegate实例亦可以用作函数参数,例如,
public static void Transform(int[] values, Transform t){ ... }
这样,Transform可以用作函数挂载,
Transform(values, Square); // values是数串,Square是符合委托定义的函数
delegate实例允许+、+=、-、-=以进行Multicast,也就是说单个委托实例可以一次执行多个函数,且允许为null——可执行函数个数为0。但是需要注意的是,在可执行函数个数为0时,任何调用式的语句都是要出错的。并且对委托进行函数增减的操作是只对应相关函数的,也就是说delegateInstance += Func1执行多次后,可以在Invoke时执行对应次数的Func1而delegateInstance -=Func2执行多次后,仅会影响委托内的Func2执行的次数(最低为0次)。
委托函数的返回值仅为最后一次执行的函数返回值——虽然之前的函数依旧会执行,所以,大部分(不需要利用这一特性)的函数都是void作为返回值的——不做特殊处理(使用out关键字)的情况下难以稳定获取。
任何的delegate类型,都隐式的继承自System.MulticastDelegate(提供一些统一方法跟变量)。
委托中的Target以及Method分别可以获取委托的对象名(指向对象的引用)以及方法名(指向方法的引用),所以在委托装入static的情况,Target为null——因为该对象不存在。
泛型与委托,可以通过结合二者编写适用性广泛的委托函数。例如,
internal delegate void Transformer<T>(T args); // 可见性需因地制宜
public class Util
{
public static void Transform<T>(T[] values, Transformer<T> delegator)
{
foreach(var x in value){ delegator.Invoke(x); }
}
}
那么,此时的Transform方法可以对任何符合Transformer格式的函数进行调取(只要values的类型与delegator一致则不会对类型进行限制)。
Func以及Action是一类已经定义好的泛型委托(2.0以前的老版本不存在这两个委托),其中Func针对带有返回值的函数,例如,
delegate TResult Func<in T1, in T2, out TResult> (T1 arg1, T2arg2);
表示两个参数带返回值的函数的泛型委托,其中TResult为返回值。
又如,
delegate void Action<in T1, in T2> (T1 arg1, T2 arg2);
同前者表示的内容相同,但是函数返回值为空。Func与Action分别支持到16个参数的函数。另外,这里可以看出in跟out两个关键字在多参数泛型中的重要作用。
委托之间不能进行赋值操作——即便两个委托的定义相同,但可以以一个委托初始化另一个委托(new);当委托指向同一个函数式,两个委托可以说是相等的;Multicast时,调用函数次序相同也可以说是相等的。
委托中可以应用逆变(contravariance)协变(covariance),逆变是指能够使用派生程度更小的类型(例如object->string),协变是指能够使用与原始指定的派生类型相比,派生程度更大的类型(例如string->object)。应用到委托中比较常见的是,委托了object作为参数的函数,使用string做Invoke参数(逆变);委托有返回值且返回值为某个特定类型时,将其用于object的初始化(协变)。深入的讲需要涉及泛型以及反编译的代码执行过程,其中有一个编译器识别的问题,如果不使用in或者out标明参数是作为输入(逆变)还是作为输出(协变),编译器会无法通过——已经使用了泛型或委托的原生类都有标明(但是部分容器依旧不能进行逆变协变,具体需要参考《CLR via C#》中泛型跟接口部分的内容)。
可有举例如下,
delegate TResult Func<out TResult>(); // 支持协变
Func<string> x = ...;
Func<object> y = x; // 扩大类型范围
delegate void Action<in T>(T arg); // 支持逆变
Action<object> x = ...;
Action<string> y = x; // 缩小类型范围
Events事件:
使用委托时,会涉及两个‘角色‘,广播者(broadcaster)与订阅者(subscriber),广播者拥有一片委托域(delegate field),它决定何时进行触发;订阅者决定何时监听,通过对广播者的委托‘+=‘或‘-=‘,订阅者之间互不相关。而事件是组织者,它的主要作用是阻止订阅者之间的互相干涉。
定义一个事件,最简单的方式就是在声明委托时加入event关键字。例如,
delegate void PriceChangeHandler(decimal oldPrice, decimal newPrice);
class Products // Broadcaster
{
public event PriceChangeHandler priceEvent; // priceEvent == null
}
这样定义之后,Products之内的代码视priceEvent为委托,而Products之外的代码只能通过+=或者-=对其进行操作。使用event关键字后,代码作用相当于,
PriceChangeHandler _priceEvent; // private delegate
public event PriceChangeHandler priceEvent // limited the access
{
add { _priceEvent += value; }
remove { _priceEvent -= value; }
}
这样做的好处有,防止其他的订阅者对委托进行修改;防止委托被清空(设置为null);防止‘意外‘的广播(类以外的委托激活)。
.Net框架有一套规定好的事件编写流程(为了方便用户与框架代码的交互),其中需要用到System.EventArgs这个类,例如,
public class PriceChangeEventArgs : System.EventArgs
{
public readonly decimal lastPrice;
public readonly decimal newPrice;
public PriceChangeEventArgs(decimal lastPrice, decimal newprice)
{
this.lastPrice = lastPrice;
this.newPrice = newPrice;
}
}
public delegate void UsrEventHandler<TEventArgs> (object source, TEventArgs e) where TEventArgs : EventArgs;
这样简单的定义了一套符合.Net框架的事件,这里需要注意的是委托需要遵守三个规则,返回值必须为void;必须有两个参数,第一个为object,第二个为EventArgs的子类,前者用于指示广播者,后者用于事件转让时的其他信息;名称必须以EventHandler结尾。紧接着可以用以上委托定义事件,
public event UsrEventHandler<PriceChangeEventArgs> PriceEvent; // 泛型
或者使用System.EventHandler直接定义事件,
public event EventHandler<PriceChangeEventArgs> PriceEvent; // 这里EventHandler的第二项参数是System.EventArgs,非泛型,不处理额外信息
订阅者通过‘+=‘跟‘-=‘对事件进行函数的注册与注销,这对应在声明event时直接产生的功能两个功能add跟remove,可以显式声明进行干预,
private EventHandler _priceChanged; // private delegate field
public event EventHandler PriceChanged
{
add { _priceChanged += value; }
remove { _priceChanged -= value; }
}
这样做在以下三种情况中会比较有用,事件极多但是同时存在的事件很少时,将委托存入字典内会比留很多个null委托节省空间;显式执行定义了事件的接口时;当作为其他广播事件的类的节点时。关于最后一点,
piblic interface IFoo { event EventHandler Ev; } // 接口,用EventHandler声明了事件
class Foo : IFoo // 实现接口的类
{
private EventHandler _ev; // private delegate field
public event EventHandler IFoo.Ev // 显式实现接口的事件
{
add { ev += value; }
remove { ev -= value; }
}
}
同方法一样,event允许的修饰词有virtual、abstract、overridden、sealed甚至是static。
Lambda表达式:
"这并不是什么新内容了",C++也有这个东西,不过不常用(作为多循环里的匿名处理段还是可以的——节省一个小函数声明,实质意义也不大的讲)。
C#内表达式的结构定义如下,
(parameters) => expression-or-statement-block
可有例子如下,
x => x * x;
表达式可以用大括号括起来,例如,
x => { return x * x; };
Lambda表达式经常同Func或Action一起联用,例如,
Func<x,x> sqr = x => x * x; // 其中x对应前一个x,x * x对应后一个x作为返回值,而sqr实际指向的是匿名函数
两个参数时,
Func<string,string,int> totalLength = (s1, s2) => s1.Length + s2.Length;
int length = totalLength("Hello", "World");
可以显式指定表达式的类型,例如,
Func<int,int> sqr = (int x) => x * x; // 可是此处Func已经限定了输入输出值的类型
可以使用外部定义的变量或表达式(这一过程称为捕获),例如,
int factor = 12;
Func<int,int> multiply = (n) => n * factor;
需要注意的是,外部变量的值以委托调用的时机为准,而不是以委托定义的位置为准。委托(表达式)可以对捕获变量进行修改,捕获变量自身的生存周期延展的跟表达式一样长。
用表达式捕获循环数时,结果依旧以调用时为准,例如,
Action[] counters = new Action[3];
for (int i = 0; i < 3; ++i)
counters[i] = () => Console.Write(i); // 这里是捕获了没错
foreach (Action a in counters) a(); // 这里才调用,i都变成3了
如果需要获得捕获值,在循环内
int capture = i;
counters[i] = () => Console.Write(capture);
即可,不过这样做在循环数i很大的时候,会有i个临时变量被延展至跟表达式所在委托一样的长度。另外,如果使用foreach替换for,结果在C#4.0与C#5.0之间会不同,虽然,理应输出不同的值。
匿名方法,同Lambda表达式类似,但是有几点不同,没有隐式类型参数;表达语法不同,匿名方法必须有大括号;编译为表达式树的能力不同。如果有委托定义如下,
delegate int Transformer (int i);
则可定义匿名方法如下,
Transformer sqr = delegate(int x){ return x * x; }; // 此处如果使用Lambda表达式,则可写为 Transformer sqr = (x) => x * x;
Console.WriteLine(sqr(3));
在捕获变量的特点上,二者倒是相同。
try与异常:
非常好笑的一件事情是,学习C++的时候引入过try...catch机制,但是使用的特别特别的少,而且将大段大段的代码放到try之内还要尾随一个catch才能正常工作显得非常笨拙。
很不幸的是,C#中的try也是这样的,将需要进行错误处理的代码部分放入try的区间之内,尾随catch区间、finally区间或者两个区间都尾随。catch区间在try的代码段发生错误时执行,用于处理Exception,记录日志,或者重新抛出Exception、更高等级的Exception等;finally区间在try或catch之后执行,不管是否发生了错误,用于清理程序——关闭网络、关闭文件等。一般其结构如下,
try{ ... }
catch(Exception eA){ ... }
catch(Exception eB){ ... }
... // other catch
finally{ ... } // clean up
异常系统只是检测机制,并非"avoid problems"的手段——而且开销不低。
在执行程序的过程中,出现异常时,CLR会判断执行位置是否在try字段内,如果是,执行位置将转为相应的catch字段,执行完之后转会try字段的下一个语句,最后执行finally字段;如果不是,执行位置跳回至上一级,测试会重复下去。
如果没有任何的代码段、功能为异常负责,结果是跳出错误信息消息框,程序终止。
catch的写法,被捕获对象为System.Exception或者System.Exception的子类,直接捕获System.Exception在以下几个情况比较有用:
1.程序不会因为特定异常无法恢复;
2.你计划再抛出异常;
3.错误处理不是第一位的,首要是终止程序;
另外,你可以通过捕获特定的异常以避免一些情况,例如,OutofMemoryException。
catch语句只对应特定的异常执行,异常可以不声明变量——如果不作处理的话,省略异常类型的catch{}可以捕获任何异常。
System.Exception的三个要素,StackTrace,catch字段内从异常源头到调用部分的所有函数;Message,错误描述;InnerException,引起外部异常的异常,如果存在的话。
finally的写法,除了无限循环跟程序意外终止之外,finally部分的代码段一定会被执行,例如,
static void ReadFile()
{
StreamReader reader = null;
try
{
reader = File.OpenText("file.txt");
if (reader.EndofStream) return;
Console.WriteLine (reader.ReadToEnd());
}
finally
{
if(reader != null) reader.Dispose(); // 即便是以上if语句为真,函数返回,这里也是要执行的,文件不存在时程序直接报错,这里就不执行了。
}
}
虽然,以上try跟finally段部分有多处不严谨的地方。
using&&IDisposable,(因为有些对象需要手动进行资源回收,例如打开文件、视图、操作系统句柄、未处理的objects等)继承自IDisposable的类,会有一个Dispose()方法用来定义资源清理,使用using来“优雅”的调用它,
using (StreamReader reader = File.OpenText("text.txt")) { ... }
这相当于
StreamReader reader = File.OpenText("text.txt");
try { ... }
finally
{
if(reader != null)
((IDisposable)reader).Dispose();
}
程序大致结构:
/* 需要注意的是Libraries跟services不需要Main作为入口,多个Main同时存在时,需要在编译器选相处使用/main选择哪一个作为入口函数。*/
using System....;
class XXX
{
// ...
}
class YYY // Main必然在class或者struct内(纯粹的C#其实是不用的)
{
/* Main的式样
public static void Main() { } public static int Main() { } public static void Main(string[] args) { } public static int Main(string[] args) { } public static async Task Main() { } // Task C# 7.1之后可用 public static async Task<int> Main() { } public static async Task Main(string[] args) { } public static async Task<int> Main(string[] args) { }
*/
static void Main(string[] args)
{
// TODO
}
}