C#复习笔记(2)--C#1所搭建的核心基础

时间:2023-12-31 18:10:02

通过对C#1所搭建的核心基础的深入了解,可以知道之后的C#版本在C#1的基础上做了很多扩展,而这些扩展都是基于C#搭建的核心基础而来的。

委托

一、编写委托的过程

委托经常和C语言的“函数指针”挂钩。委托是方法参数化、函数式语言一个重要的表达方式。C#1中编写一个委托要经过四部:

1、声明委托类型

delegate void StringProcessor(string param1);

这个委托指定了一种无返回值,有一个string类型的参数的方法。

这个委托继承自System.MulticastDelegate,后者又派生自System.Delegate.

委托本身是引用类型,所以声明委托的时候不能在方法中声明。可以作为内部类,也可以声明到namaspace下面。

  2.、必须有一个方法包含了要执行的代码,这个方法要和委托声明的签名一致,包括参数和返回值

C#一中要为委托找到一个方法的话必须要和委托的签名完全一直,在C#2中允许委托的斜边和逆变。必须下面的委托:

delegate object SayHello(string word);  

这个委托可以被下面这个方法实例化:

 static string Sayhello(object word)
{
Console.WriteLine(word);
return word.ToString();
}

3、创建委托本身


可以采用new操作符来创建一个委托的实例:

 SayHello say = new SayHello(Sayhello);

C#2中支持委托-方法组的转换,所以,可以直接使用

SayHello say = Sayhello;

4、调用委托

一切准备就绪后就可以用invoke方法来完成调用。C#还可以更简单的完成这个操作,使用

SayHello say = Sayhello;
say("hello,you");

就可以完成,不过在背后编译器帮你完成了一些工作:

C#复习笔记(2)--C#1所搭建的核心基础

 二、合并和删除委托


委托和string的特征有一些相似:都是不易变的。这体现在委托的合并和删除上面:

委托内部有一个操作列表(invocation list):System.Delegate的静态方法Combine和Remove负责创建新的委托实例。其中,Combine将两个委托的操作列表链接到一起,Remove负责将一个委托实例的一个操作删除。它们都不改变原有的类型(所以说和string很像)他们都是返回一个Delegate。

很少在代码中直接调用Delegate.Combine,Delegate.Remove而是用+=和-=操作符。这同样是编译器的功劳:

C#复习笔记(2)--C#1所搭建的核心基础

如果调用列表中抛出异常,那么抛出异常的那个方法导致调用列表不在执行。

如果有返回值,那么调用列表会返回最后调用的那个方法的值。所以一般不会在调用列表中执行有返回值的方法。

但是如果有必要,可以通过调用invocationlist来逐个执行方法并获取返回值。

三、事件

首先来看事件的声明:

public event SayHello SayHelloEvent;

事件是对委托的封装,有点儿类似于属性和后备字段的关系,上面这句声明编译器会做如下工作:

1、在相同的作用域中声明一个SayHello委托类型的私有字段

2、声明一个类似于属性的块结构,这个块结构包含一个类似于属性的取值方法和赋值方法,由编译器进行命名,前缀分别是"add"和"remove"。

3、在外部,会通过+=和-=来操作事件,声明事件时的访问修饰符会限制这一操作。就是说如果声明为一个private的事件的话外部是不能操作事件的。

4、在内部,+=操作符会调用“add”前缀的方法,“add”前缀的方法会调用Delegate.Combine来合并操作,同理,-=操作符会调用“remove”前缀的方法,该方法会调用Delegate.Remove来删除操作。

5、从上面可以看出,事件就是一对儿“add"和”remove“方法。他封装了委托,从而避免调用方直接操作委托,而是通过事件来间接的操作委托,在类的内部可以看到委托,在类外部可以看见事件。

总结一下,事件不是委托实例,只是成对儿出现的add/remove方法。类似与属性的get/set方法。

类型系统的特征

在C#4之前,C#的类型系统是静态的、显式的和安全的。

一、静态类型和动态类型

C#是静态类型的:每个变量或表达式的类型在编译时都是已知的。只有类型已知的操作才是被允许的。静态这个词用来表示使用不变的类型数据来分析哪些操作可用。

与静态类型想对应的是动态类型,动态类型的实质是变量中含有值,但那些值不限于特定的类型。所以编译器不能执行相同形式的检查。

二、显示类型和隐式类型

这个讨论只有在静态语言的环境中才是成立的。对于显式类型来说,每个变量都在声明时指定类型。隐式类型则允许编译器变量的用途来确定变量的类型。无论是显式还是隐式,表达式的类型都会在编译时就确定。

三、类型安全与类型不安全

C#是类型安全的,C#支持一些类型安全的转换:继承上的、数值上的,如果使用强制类型转换,编译器会检测转换的结果,也会抛出异常来组织程序的继续运行,C#还支持有限的协变和逆变(C#4)。但是和真正的协变和逆变还有很长一段距离。

可以通过显式实现接口来处理协变和逆变上的一些限制。

总结一下:

C# 1是静态类型的—— 编译器知道你能使用哪些成员;

C# 1是显式的—— 必须告诉编译器变量具有什么类型;

C# 1是安全的——除非存在真实的转换关系, 否则不能将一种类型当做另一种类型;

静态类型仍然不允许一个集合成为强类型的“ 字符串列表” 或者“ 整数列表”, 除非针对不同的元素使用大量的重复代码;

方法覆盖和接口实现不允许协变性/ 逆变性。

值类型和引用类型

一、值类型和引用类型的基础知识

对于引用类型的表达式(如一个变量),它的值是一个引用,而非对象。
    引用就像URL——是允许你访问真实信息的一小片数据。

对于值类型的表达式,它的值是实际的数据。

有时,值类型比引用类型更有效, 有时恰好相反。
    引用类型的对象总是在堆 上, 值类型的值既可能在栈 上, 也可能在堆上,具体取决于上下文。 
    引用类型作为方法参数使用时, 参数默认是以“值传递”方式来传递的—— 但值本身是一个引用。 

值类型和引用类型的本质区别在于复制的方式不同:值类型复制值本身,引用类型复制的是引用。

两种类型的另一个差异在于, 值类型不可以派生出其他类型。 这将导致的一个结果就是,值不需要额外的信息来描述值实际是什么类型。 把它同引用类型比较, 对于引用类型来说, 每个对象的开头都包含一个数据块, 它标识了对象的实际类型, 同时还提供了其他 一些信息。 永远都不能改变对象的类型——执行简单的强制类型转换时, 运行时会获取一个引用, 检查它引用的对象是不是目标类型的一个有效对象。如果有效, 就返回原始引用; 否则抛出异常。 引用本身并不知道对象的类型—— 所以同一个引用“ 值” 可用于( 引用) 不同类型的多个变量。 例如 下面 的 代码:

 Stream stream=new MemoryStream();
MemoryStream memory = (MemoryStream) stream;
//第1行创建一个新的MemoryStream对象, 并将stream变量的值设为对那个新对象的引用。 第2行检查stream的值引用的是不是一个MemoryStream( 或派生类型)对象,并将MemoryStream的值设为相同的值。

值类型变量本身存储的是值,引用类型变量本身存储的是引用,这个引用里面包含一个指向真正对象的地址。

变量的值在它声明时的位置存储。局部变量的值总是存储在栈( stack)中(这个结论只有 在C#1中完全成立。以后会讲到,在更高版本C#中,在特定情况下,局部变量最终可能存储在堆中。)实例变量的值总是存储在实例本身存储的地方。 引用类型实例(对象)总是存储在堆( heap)中, 静态变量也是。

    按值传递和按引用传递的区别是按值传递的是副本,按引用传递的是别名。

    装箱的背景在于值类型和引用类型的值不同:值类型的值就是值本身,而引用类型的值只是一个引用。

    值类型的值会在需要引用类型的行为时被装箱; 拆箱则是相反的过程。

构建于C#1之上的新特性

一、与委托有关的新特性

C#1中的委托-->C#2的泛型、方法组转换、匿名方法、委托协变性和逆变性-->C#3的lambda表达式、内建的Func、Action、泛型的接口和委托的协变性和逆变性

二、与类型系统有关的特性

C#2-->泛型、委托的协变性和逆变性-->C#3匿名类型、隐式类型、扩展方法-->C#4受限的泛型协变性和逆变性、动态类型

三、与值类型有关的特性

C#2-->泛型、Nullable<t>可空值类型