C#笔记——1.类型系统

C#简介:

2000年,微软向世界推出了新的编程语言C#,从C#1.0开始,每一次的版本更新都为C#增添了诸多新的特性

eg:

C#1,奠定了C#类型系统的基础,规定了引用类型和值类型的概念及委托

C#2,泛型机制、匿名方法、迭代器、可空类型

C#3,自动实现的属性、匿名类型、扩展方法、Lambda表达式、LINQ

C#4,命名参数和可选参数、泛型接口、泛型委托的协变和逆变

C#5,异步和等待

C#6,自动属性初始化以及内建字符串Interpolated String取代string.Format()函数

...

每种编程语言都有某种形式的类型系统,C#的类型系统是静态的、安全的,大部分情况下是显式的。

C#要求所有的类型都从 System.Object 类派生,这样保证所有类型都拥有一套最基本的由基类System.Object声明的方法——4个公共方法,2个受保护方法

1.Equals():若两个对象相等则返回true,否则返回false

2.ToString():默认返回类型的完整名称

3.GetType():返回一个从Type类派生的类型实例

4.GetHashCode():返回对象的值得哈希码

5.Finalize():是在对象被标记为可供GC进行垃圾回收之后,其内存真正被回收之前,会调用的方法,为虚方法

6.MemberwiseClone():创建当前对象的浅表副本,即创建一个新对象并将当前对象的非静态字段复制到新对象(值类型逐位复制和引用类型复制引用)

静态类型系统

“静态”这个词是用来描述表达式的编译时类型,编译器在编译时需要检查和使用这些静态的、不变的数据来确定哪些操作是合法的。

C#是静态的类型系统,在C#中声明一个变量时所确定的类型,便是该变量在编译时的类型,意味着在C#代码中,每一个变量都有着一个特定的类型并且该类型在编译时是确定了的

//静态类型
int year = 2018;
string code = "C#";

静态类型系统中的动态行为

//动态行为
public abstract class Product {
    public string Name { get; private set; }
    public decimal Price { get; private set; }

    public Product() { }

    public Product(string name ,decimal price) {
        Name = name;
        Price = price;
    }

    public virtual void Detail() {
        Console.WriteLine("The Product : {0} 's price is {1}$.",Name,Price);
    }
}

public class PC : Product {
    public PC() : base() { }

    public PC(string name , decimal price) : base(name,price) { }

    public override void Detail()
    {
        Console.WriteLine("The PC : {0} 's price is {1}$.", Name, Price);
    }
}

class Program
{
    static void  Main(string[] args)
    {
        Product temp = new PC("ASUS",1000);
        temp.Detail();
        Console.ReadKey();
    }
}

动态行为一:

在声明变量 temp 时,其定义的类型为 Product,即变量 temp 在编译时的类型是 Product;

之后,实例化一个 Product 类型的子类 PC类型的实例,并将该实例的引用赋值给变量 temp;

这表示,在代码编译时被声明为Product类型的 temp 变量,在运行时指向的是一块存储PC类型实例的内存,即:temp在运行时的类型是PC;

之后的代码运行编译器将会查找PC类中定义的属性和方法。

动态行为二:

在调用虚方法时,其实际实现的是依赖于所调用对象的类型 PC.

静态类型系统中的显式类型

显式类型是指在声明变量时必须显式的制定变量的类型

string name = "ASUS";
decimal price = 1000;

即,每个变量在声明时都显式的确定其类型

静态类型系统中的隐式类型

隐式类型是指在声明变量时不指明其类型,而是允许编译器根据变量的用途来推断变量的类型,但是,隐式声明的变量仍然是静态类型且在编译时其类型已经确定。

C#3 中引入 var 关键字 来表示 隐式类型,使用 var 来声明变量,编译器将在编译时对该变量进行类型推断

需要注意的是,在引入var 关键字之后,程序员可以使用 var 关键字来简化开发流程,但是,C#3 仍然是静态类型的编程语言,用 var 声明的变量类型是由编译器推断出的,在编译时是已经被确定了的。

var name = "DELL";
var price  = 1200;

//此时,编译器将会报错
name = 500;

注意: 区分显式类型与隐式类型之间的不同,仅仅在静态类型系统的编程语言中才有意义。

动态类型

动态类型与静态类型相对,即变量类型的确定是在运行时才确定的类型。

在C#4.0中引入了动态类型后,为C#语言增添了动态语言的特征,但C#仍是静态语言。C#4.0添加的 dynamic 关键字来定义动态类型。

面对动态类型,C#编译器只需完成检查语法是否正确,但无法确定所调用的方法或者属性是否正确(因为只有在运行时才知道它们的类型)

其中 dynamic 关键字不同于C#3 中的 var 关键字, var 并不是类型,var 只是一个指令,它告诉编辑器根据变量的初始化表达式来推断类型; dynamic 是类型,但是,在编译时dynamic 不属于CLR类型,运行时则一定是CLR类型中的一种。

object budget = 5;
Console.WriteLine(budget.GetType());    //System.Int32
//object类型,使用时需强制类型转换
budget = (int)budget + 5;

var price = 10;
Console.WriteLine(price.GetType());    //System.Int32
price = price + 5;

dynamic cost = 15;
Console.WriteLine(cost.GetType());    //System.Int32
//动态类型在编译时编译器完全不知道为什么类型(IDE也无法进行语法提示)
cost = cost + 1;

Console.ReadKey();

动态类型总结

一、使用动态类型对于开发人员来说没有了IDE的智能提示,但是,动态类型减少了强制类型转换的代码,增强了代码的可读性

二、使用动态类型可以在C#静态语言中调用Python等动态语言

类型安全

类型安全,本质是有关类型操作的一种规范,即不能一种类型当做另外一种类型,除非,其间存在类型转换关系。

C#是类型安全的,允许合理的类型转换,当派生类向基类的转换时,该转换可以认为是一种安全的类型转换,因此,在C#中无需特殊的语法进行向基类的隐式转换。

当需要将一个对象转换成他的某个派生类时,则需要使用显示转换来提供足够的信息给编译器。

Product temp = new PC("ASUS", 1000);

//C#编译器在编译时检查和使用这些数据来确定转型等其他操作是否合法
PC tempPC = (PC)temp;

tempPC.Detail();

同时,在程序运行时也会进行类型转换的检查操作。

值类型和引用类型

.Net Framework有值类型和引用类型的概念,由于引用类型的复杂性、大小和使用方式,引用类型会需要在内存中存在一段时间,所以当垃圾回收器Garbage Collector/GC在执行标记-清除算法时,只有引用类型需要被标记,引用类型通常分配在堆上,而值类型可以分配到栈或者堆上

引用类型

引用类型的声明

根据ECMA的C#语言规范,任何被称为类的类型都是引用类型

通常可以使用以下3个关键字来声明一个自定义的引用类型:

  • Class
  • Interface
  • Delegate

C#中内建的引用类型:

  • Dynamic
  • Object
  • string
  • ···

引用类型的创建

C#中的引用类型总是从托管堆中进行分配,所有的引用类型都需要使用 new操作符创建。

PC tempPC = new PC("DELL", 1200);

使用new操作符创建引用类型背后的步骤:

第一步:计算所需的内存空间;new操作符将会计算目标类型加上包括System.Object在内的所有基类重定义的所有实例字段所需要的字节数,除此之外,还需要一些额外的信息需要在托管堆中分配空间,例如:类型对象指针和同步索引块。

第二步:在托管堆上分配所需的内存空间;完成所需内存空间的计算之后,分配所有的字节为0。

第三步:初始化对象的类型对象指针以及同步索引块;

第四步:调用类型的实例构造器;传递在使用new操作符时的实际参数,此时,编译器会在构造器中自动调用当前类型所有基类的构造器,从调用所有类型的基类System.Object构造器开始(该构造器仅仅是返回,无其他逻辑操作),到当前子类的构造器结束,每个类型的构造器都负责初始化该类型定义的实例字段

第五步:最后会返回一个指向新建对象的引用;即声明的变量只是一个指向该类型对象的引用,而非对象本身

值类型

变量值分配的位置与该变量声明的位置有关:

局部变量的值总是存储在线程栈上,实例变量的值与实例本身一起存储在实例存储的地方,引用类型实例总是存储在堆上。

值类型的变量直接包含其值,值类型的实例一般分配在线程栈上,不受垃圾回收机制GC的影响,一些情况下分配在托管堆上。

值类型大体上可以分为 结构 和 枚举 两类:

其中,结构大体上又可以分为以下3种:

  • 数字型结构:System.Int32结构、System.Float结构、System.Decimal结构等
  • 布尔型结构:System.Boolean结构
  • 用户自定义结构体

所有结构都是派生自抽象类型System.ValueType,包括枚举的基类System.Enum都是派生自System.ValueType,且System.ValueType派生自System.Object

public enum Brand {
    Samsung,
    iPhone,
    mi,
}

public struct Phone {
    public string Name { get; private set; }
    public decimal Price { get; private set; }
    public Brand TheBrand { get; private set; }

    public Phone(string name ,decimal price ,Brand brand) {
        Name = name;
        Price = price;
        TheBrand = brand;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Phone p1 = new Phone("Note8",500,Brand.Samsung);
        Phone p2 = new Phone("8",600,Brand.iPhone);
        Phone p3 = new Phone("mix2s",500,Brand.mi);

    }
}

值类型的new操作符:

在声明一个值类型的实例时,使用new关键字是告诉编译器该实例已经被初始化;

如果不使用new关键字或者手动赋初始值,只单纯声明一个变量,此时如果直接访问该变量,则会报错。

装箱与拆箱

由于值类型不作为对象在托管堆中分配、不受垃圾回收机制的影响、不需要使用引用来引用,但是,有时我们就是需要使用一个引用而不是值类型的值,此时就需要值类型的装箱。

ArrayList testList = new ArrayList();
testList.Add(p1);

以上代码中ArrayList的Add方法的参数类型是Object类型,而实际类型为Phone结构体,为了是代码正常运行,Phone结构体的实例p1此时必须转换成真正在托管堆上分配的对象,并且获得该对象的引用。

值类型实例装箱背后的步骤:

第一步:在托管堆中分配内存,分配的内存空间除了值类型各个字段所需的内存之外,还需要加上托管堆所有对象都需要有的两个额外成员——类型对象指针和同步索引块所需的内存。

第二步:将值类型的字段复制到新分配的堆内存中。

第三步:返回对象地址,即对象的引用

装箱后创建对象的地址会返回给Add方法,并且该对象将会在托管堆中直到被GC回收。

//获取ArrayList中的元素时,我们需要告诉编译器,要将Object显式拆箱成什么类型
//此时,获取索引为0的元素的引用,并将其所指向对象的字段复制到值类型Phone的实例t1中
Phone t1 = (Phone)testList[0];

拆箱及复制背后的步骤:

第一步:获取已经装箱的对象各个字段的地址,即拆箱

第二步:将各个字段的值从托管堆上复制到线程栈上的值类型实例中,即复制

REF

深入理解C#、C#高级编程、C#游戏脚本编程、Effective C#