您的位置:首页 > 编程语言 > C#

C# 构造函数避免IL(反编译)代码膨胀的方法--C#编译有点狂啊

2017-03-31 12:42 761 查看
构造器(constructor) 是允许将类型的实例化初始化为良好状态的一种特殊方法。构造器方法在”方法定义元数据表”中始终叫.ctor(代表constructor)。创建一个引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始状态。

构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。构造器没有显式重写的所有字段保证都有一个0和null。

实例构造器永远不能被继承。不能有如下的修饰符:virtual,new,override,sealed和abstract。如果定义的类没有显式定义任何构造器,C# 编译器将定义一个默认(无参)构造器。在它的实现中,只是简单地调用了基类的无参构造器。

例如:

public class TestClass{}


等价于

public class TestClass{
public TestClass():base(){}
}


如果类的修饰符为abstract,那么编译器生成的默认构造参数的可访问性为protected;否则构造器会被赋予public可访问类。如果基类没有提供无参构造器,那么派生类必须显式调用一个基类的构造器,否则编译器会报错。如果类的修饰符为static(sealed 和abstract),编译器根本不会在类定义中生成一个默认构造器。

一个了类型可以定义多个实例构造器。每个构造器都必须有一个不同的签名,而且每个都可以有不同的可访问性。为了使代码可验证,类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器。如果派生类的构造器没有显式调用一个基类无参构造器,C#编译器会自动生成对默认的基类构造器的调用。最终,System.Object的公共无参构造器会得到调用。该构造器什么都不做,会直接返回。由于System.Object没有定义实例数据字段,所以它的构造器无事可做。

在极少数情况下,可以在不调用实例构造器的前提下创建一个类型的实例。一个典型的例子是Objcet的MemberwiseClone方法。该方法的作用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),然后将源对象的字节数据复制到新对象中。另外,用运行时序列化器反序列化对象时,通常也不需要调用构造函数。反序列化代码使用System.Runtime.Serialization.FormatterServices类型的GetUninitializedObject或者GetSafeUninitializedObject方法为对象分配内存,期间不用调用构造器。

不要在构造器中调用会影响所构造对象的任何虚方法.原因是假如这个虚方法在当前要实例化的类型的派生类型中进行了重写,就会调用重写的实现。但在继承层次结构中,字段尚未初始化。所以,调用虚方法将导致无法预测的行为 (可以看看我之前写的这篇关于继承理解的文章:http://blog.csdn.net/u010533180/article/details/52709684

C# 语言提供了一个简单的语法,允许在构造引用类型的一个实例时,对类型中定义的字段进行初始化。

class ConstructorDemo
{
private int m_x = 5;
}


构造ConstructorDemo的对象时,它的字段m_x字段初始化为5。我们看一下对应的IL代码:(IL是VS自带的反编译工具,读者可自行查阅)

.method public hidebysig specialname rtspecialname
instance void  .ctor() cil managed
{
// 代码大小       15 (0xf)
.maxstack  8
IL_0000:  ldarg.0
IL_0001:  ldc.i4.5
IL_0002:  stfld      int32  ConstructorDemo::m_x
IL_0007:  ldarg.0
IL_0008:  call       instance void [mscorlib]System.Object::.ctor() //隐式调用了Object的构造函数
IL_000d:  nop
IL_000e:  ret
} // end of method ConstructorDemo::.ctor


通过反编译可看到ConstructorDemo的构造器把值5存储到字段m_x,在调用基类(Object)的构造器。

C# 提供了这样简化的语法,允许以内联方式初始化实例字段。但在幕后,它会将这种语法转换成构造器方法中的代码来执行。这同时提醒我们注意代码的膨胀效益。何为膨胀?我们通过下面的例子来看。重点和知识点来了。

首先用一个无参构造函数和一个int构造函数来看IL代码:

class ConstructorDemo
{
private int m_x = 5;
private string m_s = "hello world";
private double m_d = 3.14159;
private byte m_b = 0xff;
public ConstructorDemo()
{

}
public ConstructorDemo(int x)
{
m_x = x;
}
}


对应的IL代码为:

无参构造函数的IL:
.method public hidebysig specialname rtspecialname
instance void  .ctor() cil managed
{
// 代码大小       54 (0x36)
.maxstack  8
IL_0000:  ldarg.0
IL_0001:  ldc.i4.5
IL_0002:  stfld      int32 NowCoderProgrammingProject.ConstructorDemo::m_x
IL_0007:  ldarg.0
IL_0008:  ldstr      "hello world"
IL_000d:  stfld      string NowCoderProgrammingProject.ConstructorDemo::m_s
IL_0012:  ldarg.0
IL_0013:  ldc.r8     3.1415899999999999
IL_001c:  stfld      float64 NowCoderProgrammingProject.ConstructorDemo::m_d
IL_0021:  ldarg.0
IL_0022:  ldc.i4     0xff
IL_0027:  stfld      uint8 NowCoderProgrammingProject.ConstructorDemo::m_b
IL_002c:  ldarg.0
IL_002d:  call       instance void [mscorlib]System.Object::.ctor()
IL_0032:  nop
IL_0033:  nop
IL_0034:  nop
IL_0035:  ret
} // end of method ConstructorDemo::.ctor

int构造函数的IL代码:
.method public hidebysig specialname rtspecialname
instance void  .ctor(int32 x) cil managed
{
// 代码大小       61 (0x3d)
.maxstack  8
IL_0000:  ldarg.0
IL_0001:  ldc.i4.5
IL_0002:  stfld      int32 NowCoderProgrammingProject.ConstructorDemo::m_x
IL_0007:  ldarg.0
IL_0008:  ldstr      "hello world"
IL_000d:  stfld      string NowCoderProgrammingProject.ConstructorDemo::m_s
IL_0012:  ldarg.0
IL_0013:  ldc.r8     3.1415899999999999
IL_001c:  stfld      float64 NowCoderProgrammingProject.ConstructorDemo::m_d
IL_0021:  ldarg.0
IL_0022:  ldc.i4     0xff
IL_0027:  stfld      uint8 NowCoderProgrammingProject.ConstructorDemo::m_b
IL_002c:  ldarg.0
IL_002d:  call       instance void [mscorlib]System.Object::.ctor()
IL_0032:  nop
IL_0033:  nop
IL_0034:  ldarg.0
IL_0035:  ldarg.1
IL_0036:  stfld      int32 NowCoderProgrammingProject.ConstructorDemo::m_x
IL_003b:  nop
IL_003c:  ret
} // end of method ConstructorDemo::.ctor


通过上面的两端IL代码 我们还没有发现有什么问题,现在我们在增加一个构造函数。

class ConstructorDemo
{
private int m_x = 5;
private string m_s = "hello world";
private double m_d = 3.14159;
private byte m_b = 0xff;
public ConstructorDemo()
{

}
public ConstructorDemo(int x)
{
m_x = x;
}
public ConstructorDemo(string s)
{
m_s = s;
m_d = 10;
}
}


我们现在只看新增这个string类型对应的IL源码:

.method public hidebysig specialname rtspecialname
instance void  .ctor(string s) cil managed
{
// 代码大小       76 (0x4c)
.maxstack  2
IL_0000:  ldarg.0
IL_0001:  ldc.i4.5
IL_0002:  stfld      int32 NowCoderProgrammingProject.ConstructorDemo::m_x
IL_0007:  ldarg.0
IL_0008:  ldstr      "hello world"
IL_000d:  stfld      string NowCoderProgrammingProject.ConstructorDemo::m_s
IL_0012:  ldarg.0
IL_0013:  ldc.r8     3.1415899999999999
IL_001c:  stfld      float64 NowCoderProgrammingProject.ConstructorDemo::m_d
IL_0021:  ldarg.0
IL_0022:  ldc.i4     0xff
IL_0027:  stfld      uint8 NowCoderProgrammingProject.ConstructorDemo::m_b
IL_002c:  ldarg.0
IL_002d:  call       instance void [mscorlib]System.Object::.ctor()
IL_0032:  nop
IL_0033:  nop
IL_0034:  ldarg.0
IL_0035:  ldarg.1
IL_0036:  stfld      string NowCoderProgrammingProject.ConstructorDemo::m_s
IL_003b:  ldarg.0
IL_003c:  ldc.r8     10.
IL_0045:  stfld      float64 NowCoderProgrammingProject.ConstructorDemo::m_d
IL_004a:  nop
IL_004b:  ret
} // end of method ConstructorDemo::.ctor


我们和上面的int构造函数的IL代码对比发现,有很多相似的IL,我们在增加一个int和string的类型的构造函数。

class ConstructorDemo
{
private int m_x = 5;
private string m_s = "hello world";
private double m_d = 3.14159;
private byte m_b = 0xff;
public ConstructorDemo()
{

}
public ConstructorDemo(int x)
{
m_x = x;
}
public ConstructorDemo(string s)
{
m_s = s;
m_d = 10;
}
public ConstructorDemo(int x, string s)
{
m_x = x;
m_s = s;
}
}


我们看一下对应的IL代码:

.method public hidebysig specialname rtspecialname
instance void  .ctor(int32 x,
string s) cil managed
{
// 代码大小       68 (0x44)
.maxstack  2
IL_0000:  ldarg.0
IL_0001:  ldc.i4.5
IL_0002:  stfld      int32 NowCoderProgrammingProject.ConstructorDemo::m_x
IL_0007:  ldarg.0
IL_0008:  ldstr      "hello world"
IL_000d:  stfld      string NowCoderProgrammingProject.ConstructorDemo::m_s
IL_0012:  ldarg.0
IL_0013:  ldc.r8     3.1415899999999999
IL_001c:  stfld      float64 NowCoderProgrammingProject.ConstructorDemo::m_d
IL_0021:  ldarg.0
IL_0022:  ldc.i4     0xff
IL_0027:  stfld      uint8 NowCoderProgrammingProject.ConstructorDemo::m_b
IL_002c:  ldarg.0
IL_002d:  call       instance void [mscorlib]System.Object::.ctor()
IL_0032:  nop
IL_0033:  nop
IL_0034:  ldarg.0
IL_0035:  ldarg.1
IL_0036:  stfld      int32 NowCoderProgrammingProject.ConstructorDemo::m_x
IL_003b:  ldarg.0
IL_003c:  ldarg.2
IL_003d:  stfld      string NowCoderProgrammingProject.ConstructorDemo::m_s
IL_0042:  nop
IL_0043:  ret
} // end of method ConstructorDemo::.ctor


在看这个对应的IL代码发现其实和上面的几个构造函数有很多重复的代码,即初始化变量。如果在新增一个字段或者新增一个构造函数,就意味每个构造函数都会有对应重复的变量初始化,尤其是新增内联字段,代码会增加的更多。如果每个构造函数对字段值进行赋值操作,那么对应生成的IL代码会更多(读者可自行尝试)这就是代码的膨胀。

对应的解决方案就是创建单个构造器来执行这些公共初始化。然后让其它构造器显式调用这个公共初始化构造器。这样可以减少生成的代码。在C#中利用this关键字来显式调用另一个构造器:对上面的代码修改如下:

class ConstructorDemo2
{
private int m_x;
private string m_s;
private double m_d;
private byte m_b;
public ConstructorDemo2()
{
m_x = 5;
m_s = "hello world";
m_d = 3.14159;
m_b = 0xff;
}
public ConstructorDemo2(int x)
: this()
{
m_x = x;
}
public ConstructorDemo2(string s)
: this()
{
m_s = s;
m_d = 10;
}
public ConstructorDemo2(int x, string s)
: this()
{
m_x = x;
m_s = s;
}
}


对应的IL代码如下:

无参:
.method public hidebysig specialname rtspecialname
instance void  .ctor() cil managed
{
// 代码大小       54 (0x36)
.maxstack  8
IL_0000:  ldarg.0
IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
IL_0006:  nop
IL_0007:  nop
IL_0008:  ldarg.0
IL_0009:  ldc.i4.5
IL_000a:  stfld      int32 NowCoderProgrammingProject.ConstructorDemo2::m_x
IL_000f:  ldarg.0
IL_0010:  ldstr      "hello world"
IL_0015:  stfld      string NowCoderProgrammingProject.ConstructorDemo2::m_s
IL_001a:  ldarg.0
IL_001b:  ldc.r8     3.1415899999999999
IL_0024:  stfld      float64 NowCoderProgrammingProject.ConstructorDemo2::m_d
IL_0029:  ldarg.0
IL_002a:  ldc.i4     0xff
IL_002f:  stfld      uint8 NowCoderProgrammingProject.ConstructorDemo2::m_b
IL_0034:  nop
IL_0035:  ret
} // end of method ConstructorDemo2::.ctor

int类型构造参数:

.method public hidebysig specialname rtspecialname
instance void  .ctor(int32 x) cil managed
{
// 代码大小       17 (0x11)
.maxstack  8
IL_0000:  ldarg.0
IL_0001:  call       instance void NowCoderProgrammingProject.ConstructorDemo2::.ctor()
IL_0006:  nop
IL_0007:  nop
IL_0008:  ldarg.0
IL_0009:  ldarg.1
IL_000a:  stfld      int32 NowCoderProgrammingProject.ConstructorDemo2::m_x
IL_000f:  nop
IL_0010:  ret
} // end of method ConstructorDemo2::.ctor

string 类型参数:
.method public hidebysig specialname rtspecialname
instance void  .ctor(string s) cil managed
{
// 代码大小       32 (0x20)
.maxstack  8
IL_0000:  ldarg.0
IL_0001:  call       instance void NowCoderProgrammingProject.ConstructorDemo2::.ctor()
IL_0006:  nop
IL_0007:  nop
IL_0008:  ldarg.0
IL_0009:  ldarg.1
IL_000a:  stfld      string NowCoderProgrammingProject.ConstructorDemo2::m_s
IL_000f:  ldarg.0
IL_0010:  ldc.r8     10.
IL_0019:  stfld      float64 NowCoderProgrammingProject.ConstructorDemo2::m_d
IL_001e:  nop
IL_001f:  ret
} // end of method ConstructorDemo2::.ctor

string 和int类型参数

.method public hidebysig specialname rtspecialname
instance void  .ctor(int32 x,
string s) cil managed
{
// 代码大小       24 (0x18)
.maxstack  8
IL_0000:  ldarg.0
IL_0001:  call       instance void NowCoderProgrammingProject.ConstructorDemo2::.ctor()
IL_0006:  nop
IL_0007:  nop
IL_0008:  ldarg.0
IL_0009:  ldarg.1
IL_000a:  stfld      int32 NowCoderProgrammingProject.ConstructorDemo2::m_x
IL_000f:  ldarg.0
IL_0010:  ldarg.2
IL_0011:  stfld      string NowCoderProgrammingProject.ConstructorDemo2::m_s
IL_0016:  nop
IL_0017:  ret
} // end of method ConstructorDemo2::.ctor


读者可以对比一下编译生成的代码少了很多。这是很推荐的一种写法。

我在附上几幅对比图,会更加明了(左侧统一为ConstructorDemo源码,右侧统一为ConstructorDemo2):

无参构造函数IL代码对比图



int构造函数IL代码对比图



string构造函数IL代码对比图



int,string构造函数IL代码对比图



参考资料:

CLR VIA C#(第3版)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息