本文承接于《cSharp继承的学习笔记-PartOne》,主要谈谈继承这一章节关于密封和接口中我认为重要的知识。
密封:对于类来说,不能继承该类;对方法来说,不能重写。
- 在《对象和类》中提到,结构本身是不支持类型继承,那是因为结构是隐式密封的。
- 在方法中或属性中使用sealed 关键字时,必须与override连用。
接口:只能包含方法,属性,索引和事件;不能包含构造函数(其本身,不允许实例化,何谈构造?),不允许运算符重载(引起.net 的其他语言不兼容的问题,如:VB)
接口可以理解为一种契约,当你和别人签订了契约,就有责任实现其中的每个方法,如若有一个方法没实现,那请不要签定该契约。当契约生效时,其他的c#代码便可通过接口知道该方法支持的特性。
在昨天的笔记中提到“结构本身不支持继承”,实际上有些武断。结构不能建立类型层次,但结构可以实现接口。
c# 的隐藏方法,这一块一直都觉得挺晦涩难懂。实际上,是没有理解出,它和虚函数之间的关系。下面以一个例子说明:
static void Main(string[] args)
{
testClass t = new testClass2();
t.myMethod();
t.my();
}
class testClass
{
public void myMethod()
{
Console.WriteLine("In base method");
}
public virtual void my()
{
Console.WriteLine("Virtual in base");
}
}
class testClass2 : testClass
{
public new void myMethod()
{
Console.WriteLine("In child method");
}
public override void my()
{
Console.WriteLine("override in derived");
}
}
- 有没有new 这个显式的声明隐藏方法的形式,不是必须的。如果没有,则编译器会还会警告你,它会默认的按照隐藏基类的方法执行。
- 从编译结果看来,隐藏函数和虚函数的区别是,override 虚函数后,用基类引用子类的实例并调用该函数,会显示其多态性。但,隐藏函数则会直接显示其基类中的相关函数。
————以下与笔记无关————
最近这两天的效率实在是低。昨天是相当于一整天只更新了一篇关于《对象和类》的日志,而今天只看了继承这一章的1/6 的内容。持续的低迷,精神状态是一方面的原因——午睡的时间给耽误了,加上早先的几天的熬夜,显得这几天看书效率低了好几个档次。另外,今天下午还是铁着心,跑出去买了一个PNY 的浪漫紫罗双子盘+一个昂达的方块糖P3 ,很心疼的说。提起U盘,那真是一头包的,月头才买不久的Sandisk 就神不知鬼不觉的不见了;提起这次买的P3,让我想起04 年那会,昂达刚出第一款vx505 时,我就买了。
如左图所示(点击查看大图),首先这一章需要理清的一对关系是,结构和类。这里尤其需要说明的是:
- 虽然结构本身不支持继承,但隐藏在他背后的还是有一条鲜为人知的继承链:Struct继承于System.ValueType,而System.ValueType继承与System.Object。除此“一脉相传”之外,你不允许给他指定任何基类,也不允许从任何结构继承点什么。在我看来,这样的框架设计也正好符合了“万事万物皆对象”的原则。
- 结构的默认构造函数会将其申明的字段初始化为标准的默认值,直接对字段进行赋值初始化会遭到编译器的拒绝。但,重载构造函数可以绕过这个限制。(c# professional 2008 P155 Constructors for Structs在这一点上貌似提的有些问题)

在我早先的知识结构中,编程这块主要是C和Java。所以对于下面提到的一些问题会有点陌生:
函数成员->方法->传递参数->out关键字。与之相对的有一个ref关键字。ref挺好理解的,就是按引用传递,貌似c中的按地址传递参数。c#中,无论是值传递,还是引用类型传递,都需要先进行初始化。但真正在编程的过程中,初始化的数据往往是不重要的,甚至有的时候函数就没用过那些被初始化的数据,自然的不论是客户还是programer都希望看到的是计算后的那些amazing的,有意义的符号。out关键字,使得程序员能够摆脱这一初始化的僵局,当在方法参数中申明一个out变量时,编译器自动对其进行初始化,该变量通过引用传送。另外,在函数体内,如果未对out参数分配一个值的话,那编译器是不会通过验证的。当然,在调用该方法时,也需要继续指明out关键字。
函数成员->方法->方法重载(帅哥说咱这个阶段暂时只用记住2点:)
- 两个方法不能仅仅只在返回类型上有区别。
- 两个方法不能仅仅只在参数申明为out还是ref来区别。
函数成员->构造函数
有一个默默奉献的默认构造函数,不论你是否重构构造函数,还是压根没有动手描述它,它总是会隐式的自动调用,并为我们将字段初始化为标准的默认值,其功能也仅限于此。不过,下述情况是个意外。
把构造函数private化(也就是传说中的对象实例化的类代理),直接实例化是不可能实现的,因为在外部,new是没有办法使用的。一般来说,这样做是为了:
- 作为储存静态成员或属性的容器。
- 仅通过调用某个静态函数成员来实例化(可以保证在程序中只有一个该类的实例,需加锁)demo如下,注意观看getAtestClassInstance方法。
class Program
{
static void Main(string[] args)
{
testClass t = testClass.getAtestClassInstance(100);
Console.WriteLine(t.b);
Console.WriteLine(t.c);
}
class testClass
{
private static int a;
public int b;
public int c;
private static testClass t;
private static object locker = new object();
private testClass(int a)
{
testClass.a = a;
c = a;
}
public static testClass getAtestClassInstance(int a)
{
if (t == null)
{
lock(locker)
{
t = new testClass(a);
}
}
return t;
}
}
}
静态构造函数,又是第一次遇到。我想事到如今,或许我们可以总结为:only one吗?请用static。如上述的getAtestClassInstance方法,保证只有一个实例运行。静态构造函数一般有这样几点值得注意和总结:
- 静态构造函数保证至多运行1次,在代码引用类之前,demo如下。
- 静态构造函数没有访问修饰符,没有其他的c#代码访问它,永远只有.net自己调用。
- 静态构造函数不能带参数,一个类只能有一个静态构造函数。
- 无参的实例构造函数与静态构造函数安全共存,因为二者的运行时间不同。
class Program
{
static void Main(string[] args)
{
Console.WriteLine(testClass.a);
new testClass();
Console.WriteLine(testClass.a);
}
class testClass
{
public static int a;
public int b;
static testClass()
{
System.Random r = new Random();
a=r.Next(10);
}
}
}
只读字段的使用规则是,只能在声明时,或构造函数中进行初始化。
扩展方法是一个静态方法,没有放在类的源代码中。虽然是静态方法,但必须使用实例来调用。如果原类中包含与扩展方法同名的方法,则扩展方法无效。
在上次写完了《垃圾回收机制的剖析》之后,就感觉这种做笔记的方法挺有意思的。于是打算把这作为一种习惯,坚持下来。下面记录的是今天看书的一点点收获。
编译运行:
- Main方法要么没有返回值Void,要么返回一个整数(int)。
- 修饰符static表示不能在类的实例上执行,因此不必先实例化类再调用(使用类本身的名称)。要访问一个实例字段,就需要使用this关键字。
变量->变量的初始化:
- 变量为类或结构的字段,如未初始化,创建这些变量时,其值就默认是0
- 方法的局部变量必须在代码中显式初始化,否则编译通不过。
常量->常量的特征:
- 常量总是静态的,不必也不允许在声明时包含修饰符static。
- 不能从一个变量提取值来初始化常量。如需相同的效果,可以使用只读变量。
预定义数据类型:
c#中把数据类型分为2种,一种是值类型,另一种是引用类型。值类型储存在堆栈中,而引用类型储存在托管堆中。引用类型与值类型的区别参考以下代码:
//引用类型给出的demo
int[] x, y;
x=new int[10];
x[0] = 10;
y = x;
y[0] = 20;
Console.WriteLine(x[0]);
//值类型给出的demo
bool a, b;
a = true;
b = a;
b = false;
Console.WriteLine(a);
这个例子的输出结果是20和True.它说明,这2个引用变量虽然进行了赋值,但托管堆中只保留了一个引用,都指向了同一块区域。而值类型则不同,当一个布尔类型变量a赋值为true之后,再将a的值赋值给b,堆栈中就会有2个布尔值。
c#认可的基本预定义类型没有内置于c#语言中,而是内置于.net Framework中。当声明一个int类型的数据时,实际上是声明了一个System.Int32的一个实例。目前看来这样的做法,有这样的好处:
- 为多语言的交互铺路,形成了一个Common Type System。
- 使得基本的数据类型可以看做是支持某些方法的类,其声明出来的数据是支持这些方法的实例。如:
int i = 10;
string str = i.ToString();
Console.WriteLine(str);
c#有15个预定义类型,13个是值类型,2个引用类型:string,object:
- 所有的整数类型的变量都可以赋予十进制或十六进制的值。
- 对一个整数是int,uint,long或ulong没有显示的声明,都默认为int。(显示的声明可以在数字后面加上L或UL)
- 代码中没有对某个非整数类型硬编码,则默认为一个double类型。
- decimal不是基本类型,所以在计算时使用会有性能的损失。
- char包含16位。一部分原因是不允许char和byte进行隐式的转换;另外,尽管8位足够编码英语中的每个字符和数字0~9,但不能够编码更大的符号系统中的每个字符(如:中文)。为了面向全世界,计算机行业正从8位字符集向16位的Unicode模式转变。
string a = "a string";
string b = a;
Console.WriteLine("a is "+a);
Console.WriteLine("b is "+b);
a = "another string";
Console.WriteLine("a is "+a);
Console.WriteLine("b is "+b);
这是我总结的c#预定义类型的mindmap,大致囊括了各个类型的基本信息。其中string类型稍微有点特殊,这里单独抽出来谈谈,尤其注意到string是预定义类型中为数不多的引用类型,代码如上。挺有意思的是,按照之前引用类型与数值类型比较的例子得出的结论,最后一条应该输出“b is another string”。可事实并不是我们想象的那样。虽然string对象保留在堆上,但string的一些常用操作会与其他引用类型有所区别:修改一个字符串,就会创建一个全新的string对象。修改a的值,不是替换了原先的,而是在堆上重新分配了一个新的对象。——这是运算符重载造成的。
流控制:
foreach循环不能修改集合中各项数据的值。虽然这个规则并不是知识体系中的漏洞,但作为之前接触不多的循环结构,还是要总结出来提醒自己。
近期评论