学习CLR via C#(第4版)(三)- 基元类型、引用类型和值类型

基元类型、引用类型和值类型

5.1编程语言的基元类型

FCL(Framework Class Library) Framework 类库,FCL是 .net Framework 包含的一组DLL程序集的统称,FCL包含了提供了很多功能,它是生成.NET Framework 应用程序、组件和控件的基础。FCL由命名空间组成。每个命名空间都包含可在程序中应用的类、结构、委托和接口等。常见的命名空间有System、System.Windows等。

什么是基元类型

编译器直接支持的数据类型称为基元类型(primitive type)。基元类型直接映射到Framework类库(FCL)中存在的类型。比如以下4行代码都是正确的,生成的IL代码也是相同的。

int a = 0;
System.Int32 a =0;
int a = new int();
System.Int32 a = new System.Inte32();

下表列出了FCL类型在C#中对应的基元类型:

C#基元类型FCL类型符合CLS说明
sbyteSystem.SByteNO有符号8位值
byteSystem.ByteYES无符号8位值
shortSystem.Int16YES有符号16位值
ushortSystem.UInt16NO无符号16位值
intSystem.Int32YES有符号32位值
uintSystem.UInt32NO无符号32位值
longSystem.Int64YES有符号64位值
ulongSystem.UInt64NO无符号64位值
charSystem.CharYES16位Unicode字符
floatSystem.SingleYESIEEE32位浮点值
doubleSystem.DoubleYESIEEE64位浮点值
boolSystem.BooleanYES一个true/false值
decimalSystem.DecimalYES一个128位高精度浮点值,常用于不容许舍入误差的金融计算
stringSystem.StringYES一个字符数组
objectSystem.ObjectYES所有类型的基类型
dynamicSystem.ObjectYES对于CLR,dynamic和object完全一致。然而,C#编译器允许使用一个简单的语法,让dynamic变量参与动态调度

可以假定C#编译器自动假定在所有的源代码文件中添加了以下using指令:

using sbyte = System.Sbyte;
using byte = System.Byte;
using int = System.Int32;
using uint = System.UInt32;
......

表达式由字面量构成,编译器在编译的时候就能完成表达式求值

Boolean found=false;//生成的代码将found设为0
Int32 x=100+20+3;//x设为123
String a="a"+"bc";//s设为“abc”

checked和unchecked基元类型操作

此指令就用来检查溢出和不检查溢出,而默认是unchecked,不过这个可以改。检查溢出就报异常,不检查溢出就回滚。

对基元类型执行许多算数运算都可能造成溢出。不同语言处理溢出也是不同的。C和C++不将溢出视为错误,并允许值回滚;应用程序"若无其事"的运行着。相反,Microsoft Visual Basic总是将溢出视为错误,并会抛出异常。

CLR提供了一些特殊的IL指令,允许编译器选择它认为最恰当的行为。CLR有一个add指令,将两值相加但不检查溢出。还有一个add.ovf指令,作用是两值相加,溢出时抛出异常。类似的还有sub/sub.ovf等。

C#允许开发人员自己决定如何处理溢出。溢出检查默认是关闭的。开发人员可以使用C#编译器控制溢出的一个办法是使用/checked+编译器开关。

C#通过提供checked和unchecked操作符来实现局部是否检查发生溢出。

unchecked:
UInt32 invalid = unchecked((UInt32)(-1));    //OK
checked:
Byte b = 100;
b = checked((Byte)(n+200))//抛出溢出异常 
  C#还提供checkedunchecked语句

checked{
    Byte b = 100;
    b = checked((Byte)(n+200))}

在Visaul Studio的"高级生成设置"对话框中可以指定编译器是否检查溢出。

System.Decimal类型是一个非常特殊的类型。虽然C#将Decimal视为一个基元类型,但CLR则不然,也就是说CLR没有相应的IL指令来决定如何处理Decimal值。Decimal值得处理速度是要慢于其他CLR基元类型的值得处理速度。还有对Decimal来说,checked和uncheked操作符、语句和编译器都是无效的,Decimal溢出时是一定会抛出异常的

5.2引用类型和值类型

CLR支持两种类型:引用类型和值类型。

值类型

虽然FCL中大多数都是引用类型,但开发人员用的最多的还是值类型。引用类型总是在托管堆上分配的,C#的new操作符会返回对象的内存地址——也就是指向对象数据的内存地址。

使用引用类型必须注意到一些性能问题,首先考虑一下事实:

1)内存必须从托管堆上分配。

2)堆上分配的每个对象都有一些额外的成员(比如前面提到过得"类型对象指针"和"同步块索引"),这些成员必须初始化。

3)对象中的其他字节(为字段而设)总是设为零。

4)从托管堆上分配一个对象时,可能强制执行一次垃圾回收操作。

如果所有类型都是引用类型,应用程序的性能会显著下降。为了提升简单的、常用的类型的性能,CLR提供了名为**“值类型”**的轻量型类型。

值类型的实例一般在线程栈上分配的(虽然也可作为字段嵌入一个引用类型的对象中)。在代表值类型的实例的一个变量中,并不包含一个指向实例的指针。相反,变量中包含了实例本身的字段。

由于变量已经包含了实例的字段,所以为了操作实例中的字段,不再需要提供一个指针。值类型的实例不受垃圾回收器的控制。因此,值类型的使用缓解了托管堆中的压力,并减少了一个应用程序在其生存期内需要进行的垃圾回收次数

.NET Framework SDK文档明确指出,在查看一个类型时,任何称为"类"的类型都是引用类型。如System.Exception类、System.Random类等引用类型。文档将所有值类型都称为结构或枚举。如System.Int32结构、System.Boolean结构等值类型。

所有值类型都必须从System.ValueType派生。所有枚举类型都从System.Enum抽象类派生,而System.Enum又是从System.ValueType派生的。CLR和所有编程语言都给予枚举特殊待遇,以后会提到。

所有值类型都是隐式密封的(sealed),目的是防止将一个值类型用于其他任何引用类型或值类型的基类型

在托管代码中,要由定义类型的开发人员决定在什么地方分配类型的实例,使用该类型的人对此并无控制权。

//引用类型
class SomeRef 
{ 
    public Int32 x; 
} 
//值类型
struct SomeVal 
{ 
    public Int32 x; 
} 
 
static void Main(string[] args) 
{ 
    SomeRef r1 = new SomeRef();     //在堆上分配
    SomeVal v1 = new SomeVal();     //在栈上分配
    r1.x = 5; 
    v1.x = 5; 
    Console.WriteLine(r1.x);                //5 
    Console.WriteLine(v1.x);                //5 
 
    SomeRef r2 = r1; 
    SomeVal v2 = v1; 
    r1.x = 8; 
    v1.x = 9; 
    Console.WriteLine(r1.x);                //8 
    Console.WriteLine(r2.x);                //8 
    Console.WriteLine(v1.x);                //9 
    Console.WriteLine(v2.x);                //5 
}

在这里插入图片描述

建议条件

除非以下条件都能满足,否则不应该将一个类型声明成值类型:

1)类型具有基元类型的行为。

2)类型不需要从其他任何类型继承

3)类型也不会派生出其他类型。

类型实例的大小应该在考虑之列,因为默认情况下,实参是以传值方式传递的,这会造成对值类型实例中的字段进行复制,从而影响性性能。同样的,被定义为返回一个值类型的一个方法在返回时,实例中的字段会赋值到调用者分配的内存中,从而影响性能。

所以,选用值类型还应满足:

1)类型的实例较小(约16字节或者更小)

2)类型的实例较大(大于16字节),但不作为方法的实参传递,也不从方法返回。

值类型的主要优势在于它们不作为对象在托管堆上分配。

值类型和引用类型的区别:

1)值类型对象有两种表示形式:未装箱(unboxed)和已装箱(boxed)。引用类型总是处于已装箱形式

2)值类型是从System.ValueType派生的。该类型提供了与System.Object定义的相同的方法。然而,System.ValueType重写了Equals方法和GetHashCode方法。由于这个默认实现存在性能问题,所以定义自己的值类型时,应该重写Equals和GetHashCode方法,并提供它们的显示实现。

3)值类型的所有方法都不能是抽象的,而且所有方法都是隐式密封(sealed)方法。

4)引用类型的变量包含的是堆上的一个对象的地址。默认情况,在创建一个引用类型的变量时,它被初始化为null,表明引用类型的变量当前不指向一个有效对象。相反,值类型初始化是,所有的成员都会初始化为0。由于值类型的变量不是指针,所以在访问一个值类型时,不会抛出NullReferenceException异常。CLR确实提供了一个特殊的特性,能为值类型 添加"可空"标识。如"int?"

5) 将一个值类型的变量赋给另一个值类型变量,会执行一次逐字段复制。将引用类型赋给另一个引用类型时,只复制内存地址。

6)由于未装箱的值类型不再堆上分配,所以一旦定义了该类型的一个实例的方法不再处于活动状态,为他们分配的内存就会被释放。这意味着值类型的实例在其内存被回收时,不会通过Finalize方法接收到一个通知。

引用类型和值类型的区别:

引用类型值类型
从托管堆中分配从线程的堆栈中分配
对象考虑垃圾回收机制不考虑垃圾回收机制
所有类都是引用结构或枚举都是值类型
System.Object继承自System.ValueType
只有装箱形式有两种形式:装箱和未装箱
可以继承和派生不能作为基类,不能有虚方法
引用类型变量初始化时默认为null初始化时默认为0值
复制时只拷贝内存地址复制时“字段对字段”的拷贝

何时使用struct, 何时使用class?

同时满足以下三个条件:

**1)**类型中没有成员会会修改类型的实例字段;

**2)**类型不需要从其它任何类型继承;

3)类型不会派生出其他任何类型;

并满足以下两个条件中的一个:

**1)**类型的实例小于16字节;

**2)**类型的实例大于16字节,但不作为方法的实参传递,也不作为方法的返回值。此时,可以把这个类型定义为struct, 否则定义为class.

定义一个值类型应该注意什么?

1)由于System.ValueType重写了Equals方法和GetHashCode方法,在定义自己的值类型时,也要重写这两个方法并提供它们的显式实现;

**2)**所有方法都不能是虚方法。

SomeVal为一个struct,包含一个实例字段x, 以下代码有何不同?

SomeVal v = new SomeVal();
SomeVal v;

两行代码都会在线程栈上分配内存空间,唯一的不同在于C#会认为new操作符对v进行了初始化。如果用第一行代码定义v, 打印v.x时会打印出默认值0, 如果使用第二行代码定义v, 打印v.x时会出错。

5.3值类型的装箱和拆箱

装箱过程

在CLR中为了将一个值类型转换成一个引用类型,要使用一个名为装箱的机制。

将值类型转换为引用类型。当我们把值类型参数传递给需要引用类型参数的方法时,会自动进行装箱操作。过程如下:

  • 从托管堆为要生成的引用类型分配大小。大小为:值类型实例本身的大小+额外空间(类型对象指针同步块索引SyncBlockIndex)。
  • 值类型的字段复制到新的分配的堆内存
  • 返回托管堆中新分配内存的地址。也就是指向对象的引用。

拆箱过程

拆箱不是直接将装箱过程倒过来。拆箱的代价比装箱低得多。拆箱其实就是一个获取一个指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。事实上,指针指向的是已装箱实例中的未装箱部分。所以,和装箱不同,拆箱不要求在内存中复制字节。还有一个重点就是,拆箱之后,往往会紧接着发生一次字段的复制操作。

获取指向对象中包含的值类型部分的指针。一般拆箱之后会进行字段拷贝操作,两个操作加起来才是真正与装箱互反的操作。过程如下:

  • 如果引用为Null,则抛出NullReferenceException异常。
  • 如果引用对象不是一个期望值类型的已装箱对象,会抛出InvalidCastException异常。
  • 返回一个指向包含在已装箱对象中值类型部分的指针。

上面第二条意味着一下代码不会如你预期的那样工作:

public static void Main(){
    Int32 x = 5;
    Object o = x;
    Int16 y = (Int16) o;//抛出InvalidCastException异常
}

在对一个对象进行拆箱的时候,只能将其转型为原始未装箱时的值类型——Int32,下面是正确的写法:

public static void Main(){
    Int32 x = 5;
    Object o = x;             //对x进行装箱,o引用已装箱的对象 
    Int16 y = (Int16) (Int32) o; //先拆箱为正确的类型,在进行装箱
}

前面说过,在进行一次拆箱后,经常会紧接着一次字段的复制。以下演示了拆箱和复制操作:

public static void Main() {  
    Point p = new Point(); //栈变量
    p.x = p.y = 1;    
    object o = p;          //对p进行装箱,o引用已装箱的实例
    p = (Point) o;         //对o进行拆装,将字段从已装箱的实例复制到栈变量
}

在最后一行,C#编译器会生成一条IL指令对o进行拆箱,并生成另一条IL指令将这些字段从堆复制到基于栈的变量p中。

再看看一下代码:

public static void Main() {  
    Point p = new Point();      // 栈变量
    p.x = p.y = 1;    
    object o = p;               // 对p进行装箱,o引用已装箱的实例

    // 将Point的x字段变成2
    p = (Point) o;              // 对o进行拆装,将字段从已装箱的实例复制到栈变量
    p.x = 2;                    // 更改变量的状态
    o = p;                      // 对p进行装箱,o引用已装箱的实例
}    

最后三行代码唯一的目的就是将Point的x字段从1变成2.为此,首先要执行一次拆箱,在执行一次字段复制,在更改字段(在栈上),最后执行一次装箱(从而在托管堆上创建一个全新的已装箱实例)。希望你能体会到装箱和拆箱/复制操作对应用程序性能的影响。

在看个演示装箱和拆箱的例子:

private static void Main(string[] args)
{
     Int32 v = 5;            // 创建一个伪装箱的值类型变量
     Object o = v;           // o引用一个已装箱的、包含值5的Int32
     v = 123;                // 将未装箱的值修改成为123
     Console.WriteLine(v + "," + (Int32)o);  //显示"123,5"
}

你可以看出上述代码进行了几次装箱操作?是3次

主要原因是在Console.WriteLine方法上。

Console.WriteLine方法要求获取一个String对象,为了创建一个String对象,C#编译器生成的代码来调用String对象的静态方法Concate。该方法有几个重载的版本,唯一区别就是参数数量,在本例中需要连接三个数据项来创建一个字符串,所以编译器会选择以下Concat方法来调用:

public static String Concat(Objetc arg0, Object arg1, Onject arg2);

所以,如果像下面写对WriteLine的调用,生成的IL代码将具有更高的执行效率:

Console.WriteLine(v + "," + o);  //显示"123,5"

这只是移除了变量o之前的(Int32)强制转换。就避免了一次拆箱和一次装箱。

我们还可以这样调用WriteLine,进一步提升上述代码的性能:

Console.WriteLine(v.ToString() + "," + o);  //显示"123,5"

现在,会为未装箱的值类型实例v调用ToString方法,它返回一个String。String类型已经是引用类型,所以能直接传给Concat方法,不需要任何装箱操作。

装箱与拆箱简单总结

装箱就是把本来在栈中的值类型,在堆中新开辟一个内存空间,把值类型的数据复制进去,并增加引用类型都有的类型指针和同步块索引,然后返回这个内存空间引用地址。

拆箱就是反过来,先获取装箱对象中各个字段的地址,再将这些字段包含的值从堆复制到栈。

由上面看出装箱拆箱其实很影响效率,所以写代码的时候应该避免。

另外装箱的值类型,调用自身的函数修改自己的字段的时候并不会修改堆里的数据,只会先转换为值类型,再修改栈里的数据。

如果要修改堆里的数据,只能定义一个接口,让值类型里的函数去实现这个接口,然后想修改堆里的数据,那么就把对象转换为这个接口,那么去修改的话,就会修改堆里的数据。

如果你看不懂上面的话,那么请慎用值类型,作者推荐不要定义任何会修改实例字段的属性或者方法,甚至可以将值类型的所有字段都加上readonly。

其实结构体什么的用类就好了,大的结构体传参啊什么的,搞不好还会引起栈溢出。毕竟每个线程也就1MB的栈空间。

一个值类型调用System.Object类定义的方法会不会发生装箱?

如果值类型重写了System.Object定义的虚方法(Equals, GetHashCode, ToString),调用时不会发生装箱,如果重写的方法中调用了基类的实现,则需要进行装箱;如果值类型调用了非虚方法(GetType, MemberwiseClone),则会发生装箱。

对象相等性和同一性

System.Object类型提供了一个名为Equals的虚方法,它的作用是在两个对象包含相同的值得前提下返回true。如:

public class Object{
    publick virtual Boolean Equals(Object obj) {
        //如果两个引用指向同一个对象,它们肯定包含相同的值
        if ( this == obj ) return true;
        //假定对象不包含相同的值
        return false;
    }
}

对于Object的Equals方法的默认实现来说,它实现的实际是同一性,而非相等性。

下面展示了如何在内部正确实现一个Equals方法。

1)如果obj实参为null,就返回false,因为在调用非静态的Equals方法时,this所标识的当前对象显然不为null.

2)如果this和obj实参引用同一个对象,就返回true。在比较包含大量字段的对象时,这一步有助性能提升。

3)如果this和obj实参引用不同类型的对象,就返回false。一个String对象显然不等于一个FileStream对象。

4)针对类型定义的每个实例字段,将this对象中的值与obj对象中的值进行比较。任何字段不相等,就返回false。

5)调用基类的Equals方法,以便比较它定义的任何字段。如果基类的Equals方法返回false,就返回false;否则返回true;

例如:

public class Object{
    public virtual Boolean Equals(Object obj) {
        //要比较的对象不能为null
        if (obj == null ) return false;
        //如果对象类型不同,则肯定不相等
        if (this.GetType() != obj.GetType()) return false;
        //如果对象属于相同的类型,那么在它们所有字段都匹配的前提下返回true
        //由于System.Object没有定义任何字段,所以字段是匹配的
        return true;
    }
}

由于,一个类型能重写Object的Equals方法,所以不能再调用这个Equals方法来测试同一性。为了修正这一问题,Object提供了一个静态方法ReferenEquals,其原型如下:

public class Object{
    public static Boolean ReferenceEquals(Object objA , Object objB) {
        retuen ( onjA == objB );
    }
}

如果想检查同一性,务必调用ReferenceEquals,而不应该使用C#的== 操作符,因为 == 操作符可能被重载

System.ValueType(所有值类型的基类)重写了Object的Equals方法,并进行了正确的实现来执行值得相等性检查。

对象的相等性和同一性简单总结

之前我们讲过System.Object提供了名为Equals的虚方法,也就是说所有的对象都是有的,作用实在两个对象包含相同值的前提下返回true。

然而这个方法只是比较了同一性,而不是相等性。

实际上就是对应的同一性就是指两个对象的引用相同,也就是说它们指向同一个对象。

相等性如字面意思可知。也就说如果具备同一性,那么一定具备相等性。

由于Equals是个虚方法,可以重写,所以并不一定就是这个用法。

(System.ValueType就重写了,Equals实现的是相等性而不是统一性。但是这个Equals里的实现步骤用到了反射,而反射这个东西又是比较慢的,所以定义自己的值类型时可以考虑重写,从而提高性能。)

而==这个操作符也是可以重载的,除非你在比较之前,将两个对象的类型都转换为object。

于是Object又提供了一个静态方法,Object.ReferenceEquals,效果就如上所述,比较同一性。

注意,如果自己去定义一个值类型,然后重写Equals方法去实现相等性。那么应该注意让类型实现IEquatable接口的Equals方法(通常实现的Equals方法除了调用自己的类型参数,还应该有一个重载函数调用object参数,以便在内部调用类型安全的Equals方法。这个定义接收object对象的重写函数么就是对IEquatable的Equals的实现),还有重写==和!=操作符方法。

考虑到排序,所以可能还需要实现IComparable的CompareTo方法,和IComparable的类型安全的CompareTo方法。实现了这些方法,那么<,<=,>,>=在内部调用类型安全的CompareTo方法也OK了。

(如果你觉得上面自己定义值类型的实现还有什么地方觉得遗漏,最好的方法其实就是去看int类型的定义就ok了)

5.4对象哈希码

FCL的设计者认为,如果能将任何对象的任何实例放到一个哈希表集合中,会带来很多好处。为此,System.Object提供了虚方法GetHashCode,它能获取任意对象的Int32哈希值。

如果你重写了Equals方法,那么还应重写GetHashCode方法。因为在System.Collection.Hashtable类型、System.Collections.Generic.Dictionary类型以及其他一些集合实现中,要求两个对象为了相等,必须具有相同的哈希码,所以重写了Equals,那么还应该重写GetHashCode,确保相等性算法和对象哈希码算法是一致的。

System.ValueType实现的GetHashCode采用了反射机制(它的速度较慢),并对类型的实例字段执行的XOR运算。建议自己实现GetHashCode,这样才能确切的掌握它所做的事,而且你的实现会比ValueType的实现快一些。

在自己实现哈希表集合时,或调用GetHashCode,千万不要对哈希码进行持久化,因为哈希码很容易改变。

小结

System.Object提供了虚方法GetHashCode,它能获取任意对象的Int32哈希码。

另外重写了Equals方法,那么最好重写GetHashCode方法。

因为在System.Collections.Hashtable类型和System.Collections.Generic.Dictionary类型以及其它的一些集合的实现中,要求两个对象必须要有相同的Hash码才被视为相等。

所以重写Equals方法实现相等性后,最好也重写GetHashCode方法,以确保相等型算法和对象哈希码算法一致。

另外重写时可以调用基类的GetHashCode方法,但是不要调用Object或者ValueType的GetHashCode方法,因为两者的实现性能不高。

包含相同值的两个不同对象应返回相同的哈希码。(作者建议不要对哈希码进行持久化,因为哈希码的算法可能会改变)

5.5dynamic基元类型

为了方便开发人员使用反射或者与基本组件通信,C#编译器允许将一个表达式的类型标记为dynamic.还可将一个表达式的结果放在一个变量中,并将变量的类型标记为dynamic,然后,可以用这个dynamic表达式/变量调用一个成员,比如字段、属性/索引器、方法、委托等。

代码使用dynamic表达式/变量调用一个成员时,编译器会生成特殊的IL代码来描述所需要的操作。这种特殊的代码称为payload(有效载荷)。在运行时,payload代码根据当前有dynamic表达式/变量引用的对象的实际类型来决定具体执行的操作。

不要混淆dynamic和var。用var声明的局部变量只是一种简化语法,它要求编译器根据一个表达式推断具体的数据类型。var关键字只能用于声明方法内部的局部变量,而dynamic关键字可用于局部变量,字段和参数。表达式不能转型为var,但可以转型为dynamic。必须实现初始化化var声明的变量,但无需初始化用dynamic声明的变量。

dynamic表达式其实与System.Object一样的类型。编译器假定你在表达式上进行任何操作都是合法的,所以不会生成任何警告和错误。但是试图在运行时执行无效操作,就会抛出异常。

不能定义对dynamic进行扩展的扩展方法,但可以定义对Object进行扩展的扩展方法。

不能将Lambda表达式或者匿名方法作为实参传给dynamic方法调用,因为编译器不能推断出要使用的类型。

C#内建的动态求值功能所产生的额外开销是不容忽视的。虽然能用动态功能简化语法,但也要看是否值得。

小结

dynamic基元类型是为了方便开发人员使用反射或者与其它非.net组件通信.

代码使用dynamic表达式/变量时,编译器生成特殊的IL代码来描述这种操作。这种特殊的代码被称为payload(有效载荷)。在运行时,payload根据dynamic表达式/变量引用的对象的实际类型来决定具体执行的操作。

dynamic类型在编译后实际上是作为System.object,然而它在元数据中被应用了System.Runtime.CompilerServices.DynamicAttribute的实例。局部变量除外,因为Attribute显然不能在方法内部使用。

另外使用的泛型的dynamic的代码时,泛型代码已经变异好了,将类型视为Object,编译器不在泛型代码中生成payload,所以也不会执行动态调度。

且编译器允许使用隐式转型语法,将表达式从dynamic转型为其它类型。

dynamic a=123;
Int32 b=a;

另外dynamic表达式的求值结果也是一个dynamic类型。

不能定义对dynamic进行扩展的扩展方法,不能将lambda表达式或匿名方法作为实参传给dynamic使用。

为COM对象生成可由“运行时”调用的包装时。Com组件的方法中使用任何Variant实际都转化为dynamic,这称为动态化。显著简化了与COM对象的操作。

当然用dynamic会有额外的性能开销,因为会引用一些必须的dll,然后执行一些动态绑定啊什么的。如果只是一两处用这个东西,还是用传统方法好一点。(一般会引用Microsoft.CSharp.dll,与com组件操作还会用到System.Dynamic.dll)

dynamic和var的区别是什么?

dynamic是在运行时检测实际类型,var是在编译时编译器已经能够确定实际类型。例如定义了一个var str = “abc”; 这完全等同于定义了string str = “abc”; 编译器可以判断出str只能是string类型,用str调用string类型的方法和属性时,智能提示可以显示所有string类型的方法和属性,但当定义dynamic str = “abc”; 时,编译器并不知道str的实际类型,只会在运行时做判断,即使我们用str调用一个根本不存在的方法,同样可以通过变异,但运行时会报错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值