The C# Programming Language Notes
1,不彻底的abstract
即使是abstract class,也不能对实现的接口的某个成员视而不见,即下面这样是不允许的:
interface in_one{
void A();
}
abstract class base_one : in_one{
}
这一点上还是Java方便,不管C#的初衷是什么
2,out参数与指针的指针
out子类不能转化为out基类,如果允许转换的化,将无法保证类型安全性;即out参数相当于指针的指针,而父类的指针与子类的指针是没有继承关系的,所以不能转化
3,强制针对接口编程
针对接口编程只是一个一般性的原则,但C#提供了一种机制,强迫客户程序员根据接口来引用你的实现类:显式接口成员
interface A{
void a();
}
class A_Sub:A {
void A.a() {
}
}
static void Main(string[] args)
{
((A)new A_Sub()).a(); //OK!
new A_Sub().a(); //Error!
}
显式接口成员实现程序跟其他成员相比,有不同的访问能力特性。因为显式接口成员不能通过实现类访问,所以,它们从感觉上是私有的。但是,由于它们可以通过接口引用访问,从感觉上它们又是公共的。
显式接口成员实现程序主要服务于两个目的:
a,类或结构实现一个客户不感兴趣的内部接口
b,用相同的签名消除接口成员的歧义
为了使显式接口成员实现程序有效,类或成员必须在它的基本类列表中命名一个接口,这个列表中包含一个成员,它的全部有效名称、类型和参数类型与显式接口成员实现程序的那些要完全一致
4,@ 取消转义
取消字符串转义尚可理解,不过连关键字都变成普通标识了,实在看不出有多大意义
5,struct的构造函数
自定义的ctor并不能隐藏默认的无参构造函数,稍有意外,却也在情理之中,默认无参构造函数对于struct有良好定义的语义
6,const,readonly
const相当于C++的static const
readonly相当于C++的const
所以,const相当于static readonly,当然,还是有些区别,const编译时获得值,static readonly运行时
public class Color {
public const Color Black = new Color(0, 0, 0);//error
public static readonly Color White = new Color(255, 255, 255);//ok
private Color(byte r, byte g, byte b) {
}
}
7,自然的boxing,unboxing
string s = "abc";
object o = s;
int i = 123;
object o = i;
对于string的例子都不会吃惊,为什么要特殊对待int的例子呢?string是System.String的别名,只要把int当作System.Int32的别名,一切不都很自然了吗?int确实是System.Int32的别名,int和string还是有区别的,boxing后的值会被复制,数组调用很明确的不会扩展到数值类型的数组
或许是效率的原因吧
8,智能的自定义转型
C++不允许一次转换中调用超过一次的自定义转型操作符,C#同样不允许,不同的是C#会在调用自定义转型操作符前后为源类型和目标类型各插入一次自动寻找的标准转型操作,如果需要的话;简单的说;
9,操作符重载
C++里内存由程序员管理,允许重载new操作符,C#则理所当然的禁止了对new的重载
C++里算术运算符+-*/等重载后,其结合形式+=,-=,*=,/=不会被自动重载,C#为了保证语义一致性,自动重载了它们的结合形式
C++里短路逻辑运算符&&,||重载后不再具有短路的特性,C#干脆禁止了对它们的重载,只能通过以下形式变通,居然保持了短路的语义:
class Test {
public static bool operator false(Test test){
return false;
}
public static bool operator true(Test test){
return false;
}
public static Test operator & (Test lhs, Test rhs){
return lhs;
}
public static Test operator | (Test lhs, Test rhs){
return lhs;
}
static void Main(string[] args)
{
if(new Test() && new Test()){
}
}
}
10,作用域
与Java有点不一样,嵌套块中名称相同的标识符都必须引用相同的实体,这项规则保证一个表达式上下文中名称的意义在一个块中是相同的:
class Test
{
double x;
void F(bool b) {
x = 1.0;
if (b) {
int x = 1;
}
}
}
是错误的,因为x在外部块中(在if语句中包含嵌套块的扩展)引用了不同的实体。相反,例子
class Test
{
double x;
void F(bool b) {
if (b) {
x = 1.0;
}else {
int x = 1;
}
}
}
是允许的,因为名称x在外部块中永远不会使用
11,离开finally
与Java一样,finally中抛出异常,原先的异常(假如有的话)将被终止;不一样的是,不允许使用return,goto等离开finally,不知为什么
12,子包与外围包
与Java不一样,子包居然能访问外围包,不用using,方便,但合理否?
13,属性权限
get/set好像不能设置不同的权限,比如,我想internal set,而public get,不知怎么弄
14,序列化
当绝大部分域可以使用默认序列化,少数需要特殊处理时,将那少数几个域做成internal独立对象,令其自定义序列化,然后原对象中包含这个独立对象
15,int[][]是一维数组
int[,]才是二维的,矩阵
int[][]实际是锯齿
int[][] one = new int[7][];
int[,] two = new int[3,2];
维指示符在最后的非数组元素前被从左到右读。例如类型int[][,,][,]是一个int类型的两维数组的三维数组的单维数组
数组调用很明确的不会扩展到数值类型的数组,例如,不存在可以允许把int[]看作object[]的转换
数组初始化程序必须有与数组维数一样的嵌套级别。最外层嵌套与最左边的维数对应,而最里层的嵌套与最右边的维数对应
16,new virtual有没有意义?
class A
{
public virtual void F() { Console.WriteLine("A.F"); }
}
class B: A
{
public override void F() { Console.WriteLine("B.F"); }
}
class C: B
{
new public virtual void F() { Console.WriteLine("C.F"); }
}
class D: C
{
public override void F() { Console.WriteLine("D.F"); }
}
class Test
{
static void Main() {
D d = new D();
A a = d;
B b = d;
C c = d;
a.F();
b.F();
c.F();
d.F();
}
}
类C和D包含两个有相同签名的虚拟方法:一个被A引入而一个被C引入。被C引入的方法隐藏了从A继承的方法。这样,D中的覆盖声明覆盖了被C引入的方法,而D覆盖被A引入的方法是不可能的。例子产生下面的输出:
B.F
B.F
D.F
D.F
什么情况下,什么理由,我们需要new virtual?
17,override时无法改变访问修饰
即使是放宽
18,x+=y 结果有可能是void
“在一个形式为x += y 或 x -= y的操作中,当x是一个事件成员而引用在x所在的类型外面发生时,操作的结果就是void;这个规则禁止外部代码直接检查事件成员” ???
19,构造函数执行顺序
可以把一个实例变量初始化函数和一个构造函数初始化函数,看作是自动插在构造函数主体中的第一条语句前。例子
class A
{
int x = 1, y = -1, count;
public A() {
count = 0;
}
public A(int n) {
count = n;
}
}
class B: A
{
double sqrt2 = Math.Sqrt(2.0);
ArrayList items = new ArrayList(100);
int max;
public B(): this(100) {
items.Add("default");
}
public B(int n): base(n – 1) {
max = n;
}
}
包含了许多变量初始化函数,并且也包含了每个形式(base和this)的构造函数初始化函数。这个例子与下面介绍的例子相关,在那里,每条
注释指明了一个自动插入的语句(自动插入构造函数调用所使用的语法不是有效的,至少用来演示这个机制)。
class A
{
int x, y, count;
public A() {
x = 1;// Variable initializer
y = -1;// Variable initializer
object();// Invoke object() constructor
count = 0;
}
public A(int n) {
x = 1;// Variable initializer
y = -1;// Variable initializer
object();// Invoke object() constructor
count = n;
}
}
class B: A
{
double sqrt2;
ArrayList items;
int max;
public B(): this(100) {
B(100);// Invoke B(int) constructor
items.Add("default");
}
public B(int n): base(n – 1) {
sqrt2 = Math.Sqrt(2.0);// Variable initializer
items = new ArrayList(100);// Variable initializer
A(n – 1);// Invoke A(int) constructor
max = n;
}
}
注意变量初始化函数被转换为赋值语句,并且那个赋值语句在对基类构造函数调用前执行。这个顺序确保了所有实例域在任何访问实例的语句
执行前,被它们的变量初始化函数初始化。例如:
class A
{
public A() {
PrintFields();
}
public virtual void PrintFields() {}
}
class B: A
{
int x = 1;
int y;
public B() {
y = -1;
}
public override void PrintFields() {
Console.WriteLine("x = {0}, y = {1}", x, y);
}
}
当new B() 被用来创建B的实例时,产生下面的输出:
x = 1, y = 0
因为变量初始化函数在基类构造函数被调用前执行,所以x的数值是1。可是,y的数值是0(int的默认数值),这是因为对y的赋值直到基类构造函数返回才被执行。
静态构造函数自动被调用,不能被显式调用。虽然提供了许多约束条件,但是静态构造函数执行的确切时间和顺序是不确定的:
一个类的静态构造函数在这个类的任何实例被创建前执行。
一个类的静态构造函数在类的任何静态成员被引用前执行。
一个类的静态构造函数在它的所有派生类的静态构造函数执行之后执行。
一个类的静态构造函数从不会被执行一次以上。
20,struct可以实现接口
稍有意外,看来struct基本类似于“对基本数据类型域有深拷贝语义的sealed类”,说完这句话都觉得别扭
21,接口
与Java不一样,接口不能声明静态常量和内部类,接口的成员必须是方法、属性、事件或索引
22,枚举
不明白为什么非得跟整型扯上点关系,为了&|操作?即使这样,也可以内部用整型来实现,不必暴露出来
23,Delegate
观察者模式在语言层面的支持,优于java.util.Observable,Delegate是类型安全的,不同于java.util.Observable,Delegate不是异常安全的
24,条件属性
通过对被调用方进行标记,代替传统的调用方条件编译,使代码整洁,单点开关,又消除了宏的缺点
推荐阅读
-
EDKII coding style(C programming language )
-
2018.09.22 上海大学技术分享 - An Introduction To Go Programming Language
-
SIMD And AVX Explanation With C# Language
-
Coursera - Programming Language - 课程笔记 - Week 4
-
Coursera - Programming Language - 课程笔记 - Week 9
-
《The C Programming Language》读书笔记(一)
-
The Java Programming Language Notes
-
The Curl Programming Language
-
The C# Programming Language Notes
-
The Programming Language Idioms