C# 原理探究 - 第 1 部分






4.79/5 (78投票s)
C# 编程语言中的 var、自动属性以及事件的 += 或 -= 如何工作。
目录
- 引言
- 它是如何工作的?
- var 和详细说明
- 自动实现属性
- 事件中的 += 和 -=
- 限制
- 相关文章
- 历史
- 参考文献
源代码和演示
引言
.NET(MSDN) 中的 C# 语言具有许多特性,例如使用 var 声明变量、自动实现属性等等,这些都使程序员的生活更加轻松。另一方面,这种抽象也会带来一些困惑,例如它是如何工作的,.NET 在后台是如何实现这些东西的。在这篇文章中,我将尝试找出其中一些东西,例如 **var**、**自动实现属性**和**事件中使用的 += 或 -=** 语法。
它是如何工作的?
在接下来的讨论中,我们将看到 **var**、**自动实现属性**和**事件的 +=** 和 **-=** 是如何工作的。
var 和详细说明
在 C# 中,有几种声明变量的方式,使用 var 关键字进行隐式类型声明就是其中之一。例如,可以使用 var totalSalary = 0; 代替 int totalSalary = 0;。正如 MSDN 的文章所建议的,var 是强类型的,但编译器会确定类型。为了更深入地探讨它,我创建了一个小类,
namespace TestHarness
{
public class VarExplorer
{
public Book ExploreVar()
{
var myBook = new Book() { Name = "What is out there?" };
myBook.Name = "What is out there?";
return myBook;
}
}
public class Book
{
public string Name { get; set; }
}
}
上面的类并没有做什么,只是初始化了一个 Book 类型的实例,该实例包含一个名为 ExplorerVar 的方法。此方法使用 var 声明了变量。在设计时或编写代码时,将鼠标悬停在 var 关键字上方,编译器将确定变量 myBook 的类型。请看下图,
图:设计时 var
因此,很明显编译器在设计时确定了类型。另一个问题是运行时发生了什么?为了测试,我编译了 TestHarness 项目 并从 bin 文件夹中获取了 TestHarness.exe,然后将其放入 .Net Reflector 程序中,使用 .Net Reflector 的帮助,我找到的 ExploreVar 方法的代码如下所示:
public Book ExploreVar()
{
Book <>g__initLocal0 = new Book {
Name = "What is out there?"
};
Book myBook = <>g__initLocal0;
myBook.Name = "What is out there?";
return myBook;
}
上面的代码清楚地表明,编译器在构建项目为 exe 时会用相应的类型替换 var 关键字。通过以上讨论,很清楚 var 在 C# 中是如何工作的。
自动实现属性
在 C# 中,类声明非常简单,我们使用以下语法声明一个类:
public class Book
{
private string name;
public string Name
{
get {
return name;
}
set {
name = value;
}
}
}
这真的很简单,但是从 C# 3.0 开始,它变得更加容易。我们可以这样声明 Book 类:
public class Book
{
public string Name { get; set; }
}
现在的问题是,用于声明类的语法的区别是什么?实际上,没有什么区别,只是编译器为我们做了所有工作。关于类的背景知识,封装的概念是以不同的方式实现的。首先声明 Book 类,通过引入属性来实现封装,这些属性实际上等同于 Get 和 Set 方法。因此,除非有公共属性或 Get/Set 方法,否则无法直接访问类的私有成员。
现在对于 Book 声明的第二种语法,编译器代表程序员完成了所有这些封装工作。当使用 .Net Reflector 反编译第二版 Book 时,编译器本身会为 Name 属性生成 get 和 set 方法,并声明一个变量 name <Name>k__BackingField(这里 <Name> 是 Book 类中定义的实际属性名),
所以 Name 的整个反编译代码如下:
private string <name>k__BackingField;
public void set_Name(string value)
{
this.<name>k__BackingField = value;
}
public string get_Name()
{
return this.<name>k__BackingField;
}
从上面的讨论可以清楚地了解编译器如何处理自动实现属性。为了在运行时进行更多调查,我创建了以下代码片段来测试运行时自动实现属性的机制:
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
public class AutoImplementatedProperties
{
public IEnumerable<string> ExploreMembers(Person personObject)
{
return personObject.GetType().GetMembers().ToList<memberinfo>().Select(memberInfo => memberInfo.Name);
}
public IEnumerable<string> ExplorePrivateFields(Person personObject)
{
return personObject.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Select(fieldInfo => string.Format("{0,40}-->{1,20}", fieldInfo.Name, fieldInfo.GetValue(personObject).ToString()));
}
public Person CreateATestObject()
{
Person personObject = new Person()
{
Name = "M",
Profession = "Coding"
};
return personObject;
}
}
public class Person
{
public string Name { get; set; }
public string Profession { get; set; }
}
}
在上面的类中,ExploreMembers 方法将探索 Person 对象的所有成员,而 ExplorePrivateFields 将探索包括 Person 对象值在内的所有私有字段。如果我们运行 ExplorePrivateFields 方法,我们将看到它将显示:
图:自动实现属性测试结果。
上面的输出,现在如果我们将其与下面的 Person 实例化代码进行比较,
public Person CreateATestObject()
{
Person personObject = new Person()
{
Name = "M",
Profession = "Coding"
};
return personObject;
}
很清楚 C# 在运行时如何存储 Name 和 Profession 属性的值。
事件中的 += 和 -=
事件的定义取自 MSDN。C# 中的事件是一种类向其客户端提供通知的方式,当对象发生某些有趣的事情时。
这次讨论的主要目的是讨论 +=(添加)或 -=(移除)语法,以及它如何与事件和委托相关联,以及它是如何工作的?实际上,用通俗的话来说,编译器会将 += 或 -= 翻译成 add 或 remove 方法。所以为了测试这个概念,我创建了一个(几乎不做任何事)的小类:
public class LogIt
{
public delegate void LogHandler(string message);
public event LogHandler Log;
public Delegate[] GetList()
{
return Log.GetInvocationList();
}
}
所以上面的类有一个名为 LogHandler 的委托和一个名为 Log、类型为 LogHandler 的事件。这个类的消费者如下:
public class AddRemoveHandlerOfEvent
{
public LogIt InitialiseLogIt()
{
LogIt logItObject = new LogIt();
logItObject.Log += new LogIt.LogHandler(Logger1);
logItObject.Log += new LogIt.LogHandler(Logger2);
logItObject.Log += new LogIt.LogHandler(Logger3);
return logItObject;
}
public void Logger1(string s)
{
Console.WriteLine(s);
}
public void Logger2(string s)
{
Console.WriteLine(s);
}
public void Logger3(string s)
{
Console.WriteLine(s);
}
}
因此,从上面的消费者类中,我们可以看到 Logger1、Logger2、Logger3 等方法已被添加到 LogIt 对象的 Log 中。现在的问题是,这些方法的信息存储在哪里,或者在何时使用 += 或 -= 语法时它是如何工作的?为了再次回答这个问题,需要借助 .Net Reflector。当我们反射代码时,我们可以看到编译器为 += 或 -= 生成了以下代码:
public class LogIt
{
// Nested Types
public delegate void LogHandler(string message);
// Fields
private LogHandler Log;
// Events
public event LogHandler Log
{
add
{
LogHandler handler2;
LogHandler log = this.Log;
do
{
handler2 = log;
LogHandler handler3 = (LogHandler) Delegate.Combine(handler2, value);
log = Interlocked.CompareExchange<loghandler>(ref this.Log, handler3, handler2);
}
while (log != handler2);
}
remove
{
LogHandler handler2;
LogHandler log = this.Log;
do
{
handler2 = log;
LogHandler handler3 = (LogHandler) Delegate.Remove(handler2, value);
log = Interlocked.CompareExchange<loghandler>(ref this.Log, handler3, handler2);
}
while (log != handler2);
}
}
// Methods
public Delegate[] GetList()
{
return this.Log.GetInvocationList();
}
}
有两个新的东西,即 add 和 remove,需要现在探索 add 和 remove 如何工作,从 add 块中的一行代码可以得到所有线索,即:
LogHandler handler3 = (LogHandler) Delegate.Combine(handler2, value);
如果我们进一步深入 Combine 方法,我们会发现类似以下的内容:
public static Delegate Combine(Delegate a, Delegate b)
{
if (a == null)
{
return b;
}
return a.CombineImpl(b);
}
这块代码来自 Delegate 类,并且 Combine 方法调用的 CombineImpl 方法定义在 MulticastDelegate 类中。
MulticastDelegate 包含两个变量
private IntPtr _invocationCount;
private object _invocationList;
而 _invocationList 是主要对象,它保存了使用 += 语法分配的所有方法签名。从下面的图片中,我们可以看到 _invocationList 如何用于将所有方法签名存储为方法指针 (IntPtr)
图:MulticastDelegate 类的 _innvocationList
图:运行时 objArray。
为了更清楚地了解这一点,我创建了一个小型测试程序来测试 .Net 如何将所有方法存储在 _invocationList 中。如果我们看 CombineImpl 方法的以下代码块,
public IEnumerable<string> GetInitialMethodPointer()
{
return GetType().GetMethods().Select(methodInfo => string.Format("{0,40}-->{1,20}", methodInfo.Name, methodInfo.MethodHandle.Value.ToString()));
}
public IEnumerable<string> GetInvocationListFrom(LogIt logItObject)
{
return logItObject.GetList().Select(delegateObject => string.Format("{0,40}-->{1,20}", delegateObject.Method.Name, delegateObject.Method.MethodHandle.Value.ToString()));
}
</string></string>
我们可以看到,GetInitialMethodPointer 方法将显示 Logger1、Logger2 和 Logger3 的初始方法指针,而 GetInvocationListFrom 方法将显示在使用 += 操作添加方法签名后 _invocationList 中包含的内容。从下图可以看出,_invocationList 包含的方法指针值与我们从 GetInitialMethodPointer 获得的值完全相同。
图:存储在 _ invocationList 中的方法指针的输出
-= 的实现也很有趣。下面的调用堆栈将显示 remove 调用如何从消费者代码一直回溯到 Delegate 类,以更新 _ invocationList 列表。
图:删除操作的调用堆栈
如果我们查看 RemoveImpl 方法的代码,我们可以看到它调用了另一个名为 DeleteFromInvocationList 的方法,如下所示:
图:RemoveImpl 方法内部
DeleteFromInvocationList 中的代码最终将更新 _ invocationList 列表,以在移除后更新当前方法签名。请看下图,
图:DeleteFromInvocationList 内部
因此,通过以上讨论,我们对 += 和 -= 如何用于事件和委托有了相当不错的了解。
如果您对 C# 语言的其他功能感兴趣,Expert C# 5.0 with the .NET 4.5 Framework 可能对您有所帮助。
局限性
- 没有讨论 List 的 .Where、.Select 扩展方法是如何工作的。
相关文章
历史
版本 1.0
参考
- C# in Depth, Second Edition, Jon Skeet
- CLR Via C#, Third Edition, Jeffrey Richter
- Understanding Mono C# Compiler, Mohammad A Rahman
- var (C# 参考)
- 自动实现属性 (C# 编程指南)
- 隐式类型化局部变量 (C# 编程指南)
- 事件教程