65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 原理探究 - 第 1 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (78投票s)

2011年10月11日

CPOL

7分钟阅读

viewsIcon

221614

downloadIcon

2524

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 的类型。请看下图,


How_does_work_in_CSharp/VarAtDesignTime.PNG

图:设计时 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 方法,我们将看到它将显示:

How_does_work_in_CSharp/OutputOfAutoImplementedProperties.PNG

图:自动实现属性测试结果。

上面的输出,现在如果我们将其与下面的 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)

How_does_work_in_CSharp/ObjectAsInvocationList.PNG

图:MulticastDelegate 类的 _innvocationList

How_does_work_in_CSharp/ObjectHoldInvocationList.PNG

图:运行时 objArray。

How_does_work_in_CSharp/EventAtRuntimeResized.PNG

图:深入查看 objArray 以从 _ innvocationList 中查找方法信息

为了更清楚地了解这一点,我创建了一个小型测试程序来测试 .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 获得的值完全相同。

How_does_work_in_CSharp/EventsOutput.PNG
图:存储在 _ invocationList 中的方法指针的输出


-= 的实现也很有趣。下面的调用堆栈将显示 remove 调用如何从消费者代码一直回溯到 Delegate 类,以更新 _ invocationList 列表。
How_does_work_in_CSharp/RemoveEvents.PNG


图:删除操作的调用堆栈


如果我们查看 RemoveImpl 方法的代码,我们可以看到它调用了另一个名为 DeleteFromInvocationList 的方法,如下所示:
How_does_work_in_CSharp/RemoveInitialCode.PNG

图:RemoveImpl 方法内部


DeleteFromInvocationList 中的代码最终将更新 _ invocationList 列表,以在移除后更新当前方法签名。请看下图,
How_does_work_in_CSharp/InsideOfDeleteOperation.PNG

图:DeleteFromInvocationList 内部

因此,通过以上讨论,我们对 += 和 -= 如何用于事件和委托有了相当不错的了解。

如果您对 C# 语言的其他功能感兴趣,Expert C# 5.0 with the .NET 4.5 Framework 可能对您有所帮助。

局限性

  • 没有讨论 List 的 .Where、.Select 扩展方法是如何工作的。

相关文章

历史

版本 1.0

参考

© . All rights reserved.