吃“软饭”的你,真的了解 .NET吗?
跟着微软混的小伙伴们,相信大家对.NET是相当熟悉了吧?对C#语言也是驾轻就熟了吧?如果你真的想用.NET来混饭吃,那么我问你,你真的了解.NET吗?
在开发过程中,你是不是会遇到某些不太理解的问题,由于赶进度顾不上仔细研究、或者不理解这些问题对继续项目没有影响,但是心里总感觉像有一根鱼刺一样如鲠在喉,时不时感到不舒服?
本文带你从一个全新的、独特的、“冷门”的视角,来近距离观察你所熟悉的.NET,你会发现原来每天与之打交道的.NET竟然变成了“最熟悉的陌生人”。有人会问了,有必要浪费时间和精力来掌握这些“冷门”知识吗?答案是肯定的,通过理解这些看似不起眼的知识,你会更好的使用你手中的.NET,比身边的人更深刻的掌握“软饭(跟着微软吃饭)”的精髓,当你应聘或跳槽的时候,你就会比别人多一个砝码;在你以后的实际开发过程中,你就会能够更加轻松愉快的解决遇到的问题。
所以,放下你的偏见,跟着本文,用心体验一个全新的.NET吧!
配置文件(App.config/Web.config)
提到.NET中的配置文件,相信大家都不会陌生,即桌面程序中的App.Config文件或者Web程序中的Web.Config文件,下面的代码就是一个配置文件的示例。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="myConfigSection" type="ConfigLib.MyConfigSection,ConfigLib"/>
</configSections>
<appSettings>
<add key="appSetting1" value="1"/>
</appSettings>
<myConfigSection myAttribute="root">
<myChild myChildAttribute="child"></myChild>
</myConfigSection>
</configuration>
代码段 1
配置文件给大家的印象应该是一个存储程序有关的配置数据的地方,例如我们经常使用的<appSettings>、<connectionStrings>等,前者用来存储自定义的程序配置、后者存储数据库的连接字符串。
不知道大家有没有注意到过在应用程序配置文件中,经常出现的一个配置节声明<configSections>……</configSections>?要想在配置文件中添加自定义的配置节,必须先在这里进行声明。如果没有在这里进行声明、而直接定义配置节的话,当读取配置文件时就会报错,这是为什么呢?
我当初就是对这个东西百思不得其解,只知道要在配置文件中定义自己的配置节,就需要在<configSections></configSections>中间先对要添加的自定义配置节进行声明,却始终不明白为什么必须这样做。这个问题一直在我脑海里挥之不去,却由于总是忙着赶进度顾不上仔细研究,只是每次看到它的时候总感觉不得劲。
我们知道,要想读取配置文件的信息,需要用到ConfigurationManager类的GetSection(string sectionName)方法,其中的参数sectionName是我们要读取的配置节名称。在调用这个方法时,.NET会现在<configSections>配置节中寻找有没有名称是sectionName的<section>声明,没找到的话会抛出异常;
若能找到,则根据对应的tpye属性,即代码段2中的【type="ConfigLib.MyConfigSection,ConfigLib"】,找到其对应的配置节处理程序,即ConfigLib命名空间中的MyConfigSection类,并利用反射得到该类的实例;
<configSections>
<section name="myConfigSection" type="ConfigLib.MyConfigSection,ConfigLib"/>
</configSections>
代码段 2
然后,利用该实例来得到自定义配置节中的各种信息。代码段3中的变量cfg就是一个MyConfigSection实例,通过该实例,便可以得到自定义配置节中的各种信息。
var cfg = ConfigurationManager.GetSection("myConfigSection") as MyConfigSection;
var x = cfg.MyAttribute;
代码段 3
下面,我们看一下MyConfigSection类的定义(代码段4)。这个类继承于ConfigurationSection类,只需要在其子类中声明自定义配置节中需要添加的属性,并用ConfigurationProperty特性进行标识即可。比如,代码段4中的MyAttribute属性,对应的是配置文件中的myAttribute="root"。
public class MyConfigSection : ConfigurationSection
[ConfigurationProperty("myAttribute")]
public virtual string MyAttribute
return (string)this["myAttribute"];
this["myAttribute"] = value;
[ConfigurationProperty("myChild")]
public virtual MyChildConfigElement MyChild
return (MyChildConfigElement)this["myChild"];
this["myChild"] = value;
代码段4
也就是说,要想实现自定义的配置节,必须指定其处理程序,而该配置节处理程序需要继承于ConfigurationSection类。在.NET早期版本(1.0及1.1版)中,配置节处理程序必须实现IConfigurationSectionHandler接口,该接口现在已被否决。不过,我们可以简单看一下这个被否决的IConfigurationSectionHandler接口,它仅有一个方法Create方法,用来创建节处理程序对象,其方法原型见代码段5。这个Create方法的最后一个参数section便是我们要读取的自定义配置节的XML对象,可以在这个方法中具体的构造需要的数据结构并返回。
public interface IConfigurationSectionHandler
object Create(object parent, object configContext, XmlNode section);
代码段5
这里需要说明的一点是,即使不在<configSections>中事先声明,我们完全也可以通过直接读取xml文件,从而得到对应的自定义配置节信息。
另外,细心的同学会发现,并不是所有的配置文件中都有<configSections>这个节,而且诸如<appSettings>、<connectionStrings>之类的配置节好像并没有在<configSections>中进行事先声明,这是怎么回事呢?刚才不是说所有的配置节都必须先在<configSections>中进行事先声明么?
下面我们来分析一下其中原因。其实,<appSettings>、<connectionStrings>之类的配置节也在<configSections>中进行了事先声明,只不过不是在我们客户程序集所在的配置文件中,而是在一个叫做Machine.config的配置文件中。我们打开本地目录【C:\Windows\ http:// Microsoft.NET \Framework\v4.0.30319\Config】,便可以找到这个Machine.config文件,用记事本打开这个文件,你会眼前一亮,原来在Machine.config文件中的<configSections>配置节里面定义了<appSettings>、<connectionStrings>等,.NET在查找用户的配置文件之前,会现在Machine.config文件中进行查找。
【注意】每个配置文件中只允许存在一个<configSections>元素,并且,如果存在该元素,它还必须是根<configuration> 元素的第一个子元素。
事件(Event)的本质
用.NET进行开发工作,相信大家对“事件”这一概念是再熟悉不过的吧?不过大家真的了解事件么?如果问“事件”的定义,大家都会说的头头是道,“事件是.NET用来传递消息的一种机制,是对象发送的消息,以发信号通知操作的发生”。那么,事件的本质是什么呢?
关于事件,咱们先稍微做一下铺垫。假如有如图所示的A、B两个对象,那么我问你,如果A的状态发生变化,怎么能让B知道A发生了变化?学过单片机的同学可能会更了解这个机制,因为单片机中有两个概念,即“轮询”和“中断”。
什么是“轮询”呢?说白了就是B以一定的频率不断的询问A,“A,你的状态发生变化了没有?”,若A发生了改变,B便会知道;
至于“中断”,就是当A发生变化时,主动地通知B,告诉B说“B,我的状态发生改变啦!”,而不用B总跑过去问。
就拿咱们平时生活中收快递的情形举例子吧,现在网购这么流行,大家都免不了在淘宝上淘些便宜货,也就离不开收快递。有些地方有专门替人收快递的“代收点”,快递来了直接把你的东西扔到代收点,甚至都不给你打电话。这里咱们先不考虑这种做法合不合理,但这是真实存在的。在这种情况下,假如你在网上买了个手机,你怎么知道你的快递到了没有?
有两种方法,一是你有事没事就需要去快递代收点跑一趟,看一下有没有你的快递;另一种方法是,快递到了以后,快递代收点给你打电话通知你去取。这其实就分别对应了“轮询”和“中断”两个概念。
说了这么题外话,这些和我们这里讨论的“事件”有什么关系呢?其实,“中断”的原理和“事件”是一样的,都是当状态改变的时候,主动通知另一方。
好了,接下来我们看一下通常在代码中是如何声明一个事件的(代码段6),首先声明一个委托类型,然后声明该委托类型的一个实例,并在前面用关键字event修饰。这说明事件本质上就是一个委托,只不过用event关键字进行修饰,告诉编译器这不是一个普通的委托实例,而是一个事件。
public delegate void MyDelegate();
public event MyDelegate MyEvent;
代码段 6
值得一提的是,事件分为“事件字段”与“事件属性”两种,我们常见的如代码段6中的定义方式是“事件字段”,而所谓的“事件属性”则需要用到一个叫做“事件访问器”的概念。代码段7中的MyEvent1就是“事件字段”,MyEvent2是“事件属性”,MyEvent2中的add/remove代码块就是所谓的“事件访问器”。
//事件字段
public event Action MyEvent1;
private Action _MyEvent2;
//事件属性
public event Action MyEvent2
MyEvent2 = (Action)Delegate.Combine(MyEvent2, value);
remove
MyEvent2 = (Action)Delegate.Remove(MyEvent2, value);
代码段 7
其实,无论是事件字段(代码段7中的MyEvent1)、还是事件属性(代码段7中的MyEvent2),编译器最终都会把它们转化成类似add_XXX、remove_XXX的方法(如下图),而这些方法内部正是使用了Delegate.Combine()、Delegate.Remove()方法。
需要注意的是,事件机制其实借助了设计模式中的“观察者模式”。下面我们先回顾一下观察者模式,UML类图如下所示。
代码段8是观察者模式的一个具体代码示例,简要介绍了观察者模式的实现方法。可见观察者模式是一种“发布/订阅”机制,而.NET中的事件也采用了同样的原理。
/// <summary>
/// 抽象通知者
/// </summary>
public abstract class Email
/// <summary>
/// 观察者集合
/// </summary>
private IList<Reader> readers = new List<Reader>();
/// <summary>
/// 添加观察者
/// </summary>
/// <param name="observer"></param>
public void Attach(Reader observer)
readers.Add(observer);
/// <summary>
/// 移除观察者
/// </summary>
/// <param name="observer"></param>
public void Detach(Reader observer)
readers.Remove(observer);
/// <summary>
/// 通知所有观察者
/// </summary>
public void Notify()
foreach (var o in readers)
o.Update();
/// <summary>
/// 具体通知者
/// </summary>
public class SohuEmail : Email
public string Message { set; get; }
/// <summary>
/// 抽象观察者
/// </summary>
public abstract class Reader
public abstract void Update();
/// <summary>
/// 具体观察者
/// </summary>
public class Jim : Reader
private SohuEmail email;
public Jim(SohuEmail m)
email = m;
public override void Update()
Console.WriteLine(string.Format("Jim received a msg:{0}", email.Message));
/// <summary>
/// 具体观察者
/// </summary>
public class Lily : Reader
private SohuEmail email;
public Lily(SohuEmail m)
email = m;
public override void Update()
Console.WriteLine(string.Format("Lily received a msg:{0}", email.Message));
/// <summary>
/// 客户代码
/// </summary>
public class ClientObserverPattern
public void Main()
SohuEmail email = new SohuEmail();
email.Attach(new Jim(email));
email.Attach(new Lily(email));
email.Message = "Good News!";
email.Notify();
代码段 8
现在我们回过头来仔细审视一下事件,由于事件本质是一个委托,而.NET中的委托(delegate)属于多播委托(即多路广播委托),一个委托可以调用一种或多种方法,并可以用在组合操作中。也就是说,借助于多播委托,事件可以接受多个事件订阅者,就相当于代码段8中的那个观察者集合readers。不同的是,给事件添加或移除订阅者用的是“+=”、“-=”操作符,而不是观察者模式中的Attach()、Detach()方法。
其实,事件机制也可以用类似于观察者模式中的Attach()、Detach()方法来实现,方法是调用Delegate类的Combine()方法和Remove()方法(见代码段7中的MyEvent2)。Delegate.Combine()方法的作用是将两个委托的调用列表连接在一起,Delegate.Remove()方法从一个委托的调用列表中移除另一个委托的最后一个调用列表。
事件的“+=”运算符可以用Delegate.Combine()方法来实现,下面的两种实现方式是等价的:
MyEvent1 += new Action(EventCase_MyEvent1);
MyEvent1=(Action)Delegate.Combine(MyEvent1,new Action(EventCase_MyEvent1));
同理,“-=”运算符可以用Delegate.Remove()方法来实现。
WPF中窗口类中的partial关键字
不知道大家有没有注意到一个现象,就是在WPF窗体类的后端文件(xxx.xaml.cs)中,窗体类的定义总是用关键字partial来修饰(代码段9),使其成为一个分部类。为什么要这样做呢?
public partial class MainWindow : Window
public MainWindow()
InitializeComponent();
代码段 9
我们知道,partial修饰的分部类作用是,让若干个同名的、也被partial关键字修饰的分部类可以合并成为一个类,通常用在类的某些部分是由工具自动生成的场合。代码段9中之所以要用到partial关键字,我们猜测到,在别的地方肯定存在和该窗体类同名的分部类。这个类在哪里呢?
在代码段9中调用了一个InitializeComponent()方法,这个地方比较可疑,当前类中并没有定义这个InitializeComponent()方法,却可以直接调用。我们用鼠标单击这个方法调用,按F12转到其定义处,会发现转到了一个很奇怪的文件中,文件名是MainWindow.g.i.cs。在这个文件中,定义了刚才那个InitializeComponent()方法,还有一下其他的成员,如Connect()方法。那么这个MainWindow.g.i.cs是什么东西呢?
WPF中有一个专门用于编译XAML文件的编译器,叫做标记编译器(MarkupCompiler)。当我们按下F6对WPF项目进行构建的时候,标记编译器首先会解析XAML文件,根据其内容,自动生成对应的.g.cs或.g.i.cs文件(二者的区别后文会介绍),该文件便是和窗体类同名的分部类。
在这里我们可以简单看一下标记编译器是如何把XAML文件编译成CLR类的。代码段10是代码段9对应的XAML文件内容,而代码段11则是编译器生成的.g.i.cs代码。编译器看到XAML文件的顶级根节点是一个Window,就在.g.i.cs中定义一个System.Windows.Window的子类,这个子类的名称是代码段10中的x:Class="WpfCase.MainWindow"。另外,编译器让该类实现IComponentConnector接口。
<Window x:Class="WpfCase.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
</Grid>
</Window>
代码段 10
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
private bool _contentLoaded;
/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
public void InitializeComponent() {
if (_contentLoaded) {
return;
_contentLoaded = true;
System.Uri resourceLocater = new System.Uri("/WpfCase;component/mainwindow.xaml", System.UriKind.Relative);
#line 1 "......\MainWindow.xaml"
System.Windows.Application.LoadComponent(this, resourceLocater);
#line default
#line hidden
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
void
System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
this._contentLoaded = true;
代码段 11
在.g.cs或.g.i.cs文件对应的分部类中,定义了InitializeComponent()方法和Connect()方法,通过观察.g.cs或.g.i.cs文件内容可知,该类实现了一个IComponentConnector接口,刚才提到的InitializeComponent()方法和Connect()方法就是这个接口的成员。
WPF在编译过程中产生了一些文件,打开项目目录下的“obj\x86\Debug”,你会发现一些乱七八糟的文件,比较重要的是.baml、.g.cs、.g.i.cs文件。其中,.baml文件是二进制文件,作为二进制资源嵌入到程序集当中;g.cs和g.i.cs文件是编译器在编译过程中生产的源文件,其中的g是generated的意思。
下面说一下.g.cs和.g.i.cs区别:程序在没有经过编译的时候,生成的源文件是.g.i.cs,其中的i是intellisence(智能感知)的意思;编译之后,才生成源文件.g.cs。
最后,提示一点,只有在XAML中设置了name的对象,才在g.cs文件中有对应的字段成员。
自定义属性如何在属性窗口显示
在WinForm项目中,我们经常会自定义一些用户控件,而这些用户控件又会定义一些自定义属性,如代码段12中的MyProperty属性。
public partial class MyControl : UserControl
public string MyProperty { set; get; }
public MyControl()
InitializeComponent();
代码段 12
当把自定义控件添加到窗体上以后,选中该控件,查看其属性窗口,会发现刚才自定义的MyProperty属性(图)。在属性窗口中,MyProperty属性默认被放在一个叫做“杂项”的分类中。我们知道,对于.NET内置的属性,通常被放置在一个有意义的分类中,比如BackColor属性属于“外观”分类、Size属性属于“布局”分类,并且当用鼠标选中某一属性时,在属性窗口最下方会有该属性的描述信息,这是怎么做到的呢?
就需要借助一些内置特性(Attribute)来实现了,常用的特性有Category、Description、Browsable及Obsolete等。代码段13中给MyProperty添加了若干个特性,其中,[Category("MyCategory")]表示该属性属于“MyCategory”分类,[Description("This is MyProperty")]表示当选中该属性时属性窗口下方显示的描述信息,[Browsable(true)]表示该属性可以在属性窗口显示,[Obsolete("behind the times")]表示该属性已经过时。
public partial class MyControl : UserControl
[Category("MyCategory")]
[Description("This is MyProperty")]
[Browsable(true)]
[Obsolete("behind the times")]
public string MyProperty { set; get; }
public MyControl()
InitializeComponent();
代码段 13
代码段13定义的MyProperty属性在VS的属性窗口界面如下图所示:
为什么通过给自定义属性添加这些特性(Attribute),就可以在属性窗口显示特定信息或执行特定分类呢?当我们给自定义属性添加指定特性之后,VS会查找自定义属性应用了哪些特性,并根据这些特性来具体的执行特定的显示逻辑。
其实Visual Studio IDE本身也是一个程序,而属性窗口实际上是一个PropertyGrid对象。不信的话,在WinForm的设计界面打开左侧的“工具箱”,在其中就能找到这个PropertyGrid控件。还可以把该控件添加到我们的窗体当中,把PropertyGrid控件的SelectedObject属性设置为我们的自定义控件(即代码段13中所示的MyControl),从而实现下图的效果。注意,这是我们的程序运行起来的界面,而不是VS IDE的设计界面。
程序集、模块、类型(Assembly/Module/Type)
我们平时经常会看到“程序集”的概念,那么到底什么是程序集呢?程序集是由一个或多个模块/资源文件组成的,是重用、安全性以及版本控制的最小单元,可以将.exe与.dll文件认为是一个程序集。
还有一个常见的概念是“模块”,它是一种可加载单元,它可以包含类型声明和类型实现。模块包含的信息足以使公共语言运行库在模块加载时找到所有的实现位。模块的格式是Windows可移植可执行(PE)文件格式的扩展。在部署时,模块总是包含在程序集中。模块是一个可移植可执行文件(例如type.dll或application.exe),该文件由一个或多个类和接口组成。单个模块可包含多个命名空间,而一个命名空间可跨越多个模块。
通常,我们用VS开发应用程序时,默认是单模块的程序集。当然,可以人为控制使其生成多个模块,下面举例说明。
新建一个控制台项目CompileModule,添加两个源文件Cat.cs和Dog.cs,如下:
using System;
namespace Animal
public class Dog
public void Say()
Console.WriteLine("I'm a dog!");
代码段
using System;
namespace Animal
public class Cat
public void Say()
Console.WriteLine("I'm a cat!");
代码段
入口方法如下:
using System;
using Animal;
namespace CompileModule
class Program
static void Main(string[] args)
Cat cat = new Cat();
cat.Say();
Console.ReadKey();
代码段
接下来鼠标点击下图所示菜单项:
打开VS的命令提示窗口(如下图):
在命令提示符窗口中,依次输入如下命令,把当前路径定位到控制台项目CompileModule所在目录。
D:
cd D:\RealPro\edu\你不知道的.NET\Case\CompileModule
然后,依次运行如下命令,会发现“D:\RealPro\edu\你不知道的.NET\Case\CompileModule”路径下,多了两个.netmodule文件,这两个文件就是所谓的“模块”。
csc /target:module Cat.cs
csc /target:module Dog.cs
接下来,运行如下命令,当前目录下会增加一个animal.dll文件,这就是一个程序集。
csc /target:library /addmodule:Cat.netmodule,Dog.netmodule /out:animal.dll
最后,运行如下命令,生成最终的控制台程序Program.exe。
csc /target:exe /r:animal.dll Progarm.cs
此时我们得到Dog.netmodule、Cat.netmodule、animal.dll和Program.exe四个文件,其中Dog.netmodule和Cat.netmodule便是模块,animal.dll和Program.exe是程序集,Cat、Dog是类型。
一个程序集可以只有一个模块,也可以有多个模块。而一个程序集分为多个模块是有意义的,原因是:CLR是以程序集为基本单位进行加载的,但是,CLR只会加载被引用到的模块,没有引用的模块不会被加载。因此,将程序集分为多个模块,运行程序时,只要保证有被引用的模块存在即可,可以减少加载的程序集大小。
就拿刚才的示例来说,由于代码段*中,只有Cat类被引用,所以只要Cat.netmodule这个模块存在就可以了,不需要Dog.netmodule的存在。不信的话,可以把Dog.netmodule文件删除,Program.exe照样能够运行;而如果把Cat.netmodule删除的话,运行Program.exe时便会报错(如下图),程序提示“文件未找到:未能加载文件或程序集‘Cat.netmodule’或它的某一个依赖项”。
VS的bin/obj目录的区别
Bin是Binary的缩写,存放的是编译的结果;而obj是Object的缩写,用于存放编译过程中生成的中间临时文件。
在.NET中,编译是分模块进行的,编译整个完成后会合并为一个.DLL或.EXE保存到bin目录下。
因为每次编译时默认都是采用了增量编译,即只重新编译改变了的模块,obj用于保存每个模块的编译结果,用来加快编译速度。
在bin或者obj目录中通常会有一些扩展名为pdb的文件,这个文件平时非常不引人注意,你甚至都不会注意到它的存在。pdb是Program Debug Database的缩写,它被称为“调试文件”,正是因为有了它,我们才可以在VS中对我们的代码设立断点以及单步调试。
pdb文件存储了VS调试程序时所需的基本信息,主要包括源文件名、变量名、函数名、帧指针、对应的行号等。因为存储的是调试信息,所以一般pdb文件是在Debug模式下才会生成。每个模块只会生成一个相同名字的pdb文件,并且模块生成的同时,会校验pdb文件生成GUID记录在模块内。
pdb文件中记录了源文件路径的相关信息,所以在载入pdb文件的时候,就可以将相关调试信息与源码对应。这样就可以以可视化的形式,实时查看调试时的函数调用、变量值等相关信息。
有的同学肯定对pdb是如何加载的很感兴趣,下面就简单说一下其加载过程。
上文提到过,.NET程序以程序集(Assembly)作为最基本的加载单元,当我们对源代码进行编译时,编译器会为每个程序集生成一个对应的pdb文件。当用调试器进行调试时,首先会查找是否存在对应的pdb文件并加载。有了pdb文件,我们便可以在调试器中方便的进行跟踪调试了。
另外,如果要想查看调试器加载了哪些调试符号,当程序运行到断点时,鼠标右键弹出快捷菜单,单击“符号加载信息”,便会弹出对话框,显示有哪些pdb文件已经被加载。
MSBuild(构建)
用惯了IDE的朋友在生成项目时,会非常熟练的按下键盘F5或者点击那个绿色的小三角图标,VS就会为我们自动编译链接并生成该项目,一切都是水到渠成、浑然天成,但这背后究竟发生了什么?VS究竟是如何完成幕后的编译、链接、生成工作的?这都得益于一个叫做MSBuild的概念。
MSBuild的全称是Microsoft Build Engine,它是Microsoft和Visual Studio的新的生成平台。MSBuild在如何处理和生成软件方面是完全透明的,使开发人员能够在未安装Visual Studio的生成实验室环境中组织和生成产品。
MSBuild引入了一种新的基于XML的项目文件格式,这种格式容易理解、易于扩展并且完全受Microsoft支持。MSBuild 项目文件的格式使开发人员能够充分描述哪些项需要生成,以及如何利用不同的平台和配置生成这些项。另外,项目文件的格式还使开发人员能够创作可重用的生成规则,这些规则可以分解到不同的文件中,以便可以在产品内的不同项目之间一致地执行生成。
MSBuild涉及到如下几个概念:
1、 项
项表示对生成系统的输入,它们根据用户为其定义的集合名称分组为项集合。这些项集合可用作任务参数,任务使用集合中包含的单个项执行生成过程的步骤。通过创建一个元素并将项集合的名称作为ItemGroup元素的子元素,可以在项目文件中声明项。例如,下面的代码创建一个名为Compile的项集合,其中包括两个文件。
<ItemGroup>
<Compile Include = "file1.cs"/>
<Compile Include = "file2.cs"/>
</ItemGroup>
使用语法@(ItemCollectionName)在整个项目文件中引用项集合。例如,可以使用@(Compile)引用上面的示例中的项集合。
2、 属性
属性表示可用于配置生成的键/值对。
通过创建元素并将属性的名称作为 PropertyGroup 元素的子元素来声明属性。例如,以下代码创建了一个值为 Build 并且名为 BuildDir 的属性。
<PropertyGroup>
<BuildDir>Build</BuildDir>
</PropertyGroup>
使用语法 $(PropertyName) 在整个项目文件中引用属性。例如,可使用 $(BuildDir) 引用上面的示例中的属性。
3、 任务
任务是MSBuild项目用于执行生成操作的可执行代码的可重用单元。例如,任务可能编译输入文件或运行外部工具。而任务一旦被创建,就可以由不同项目中的不同开发人员共享和重用。
任务的执行逻辑在托管代码中编写,并使用UsingTask元素映射到MSBuild。您可以通过创作一个实现ITask接口的托管类型来编写自己的任务。
MSBuild附带了许多常见的任务,例如Copy(用于复制文件)、MakeDir(用于创建目录)和Csc(用于编译Visual C#源代码文件)。
通过创建一个元素并将任务的名称作为Target元素的子元素,可执行MSBuild项目文件中的任务。任务通常接受参数,参数作为元素的属性传递。可以使用MSBuild的项集合和属性作为参数。例如,以下代码调用MakeDir任务并向它传递在前面示例中声明的BuildDir属性的值。
<Target Name="MakeBuildDirectory">
<MakeDir
Directories="$(BuildDir)" />
</Target>
4、 目标
目标按特定的顺序将任务组合到一起,并将项目文件的各个部分公开为生成过程的入口点。
目标通常组合为逻辑部分以允许扩展并提高可读性。通过将生成步骤拆分为许多目标,可以从其他目标中调用生成过程的一个部分,而不必将那部分代码复制到每一个目标中。例如,如果生成过程的多个入口点需要引用以便被生成,可以创建一个生成引用的目标并从每一个必需的入口点运行此目标。
使用Target元素在项目文件中声明目标。例如,以下代码创建一个名为Compile的目标,然后此目标使用在前面的示例中声明的项集合调用Csc任务。
<Target Name="Compile">
<Csc Sources="@(Compile)" />
</Target>
在Visual Studio中单击“生成”命令将执行项目中的默认目标,通常是名字为Build的Target。如果选择“重新生成”或“清理”命令,将尝试执行项目中的同名目标,即名字分别为Rebuild和Clean的Target。单击“发布”将执行项目中的名为PublishOnly的目标。这些Target的定义可以在类似如下目录中找到:
【C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Microsoft.Common.targets】
我们可以用记事本打开任意一个.NET项目对应的.csproj文件(如下),在其中会找到【DefaultTargets="Build"】,当我们单击“生成”命令时,VS会通知MSBuild去执行名字为Build的目标,从而生成项目。
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{53D2A3B3-33CA-4AC8-8C21-F9CFCCECC9FB}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>SlnSuoCsprojStudy</RootNamespace>
<AssemblyName>SlnSuoCsprojStudy</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<TargetFrameworkProfile>Client</TargetFrameworkProfile>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
</Project>
其实,.csproj文件本质上是MSBuild的项目格式,只不过Visual Studio使用MSBuild项目文件格式来存储托管项目的生成信息。Visual Studio使用MSBuild的承载实例来生成托管项目,这意味着可以在Visual Studio中和从命令行生成托管项目(甚至可以不安装 Visual Studio),最后将得到相同结果。我们完全可以用记事本手写一个.csproj文件,只有符合既定格式,就可以用IDE打开。
下面我们打开任意一种文本编辑器,记事本也行,这里我们用的是UltraEdit。
1、新建一个文档,输入下面的代码,另存为“MyProj.csproj”文件。这时我们就得到了一个简单的不能再简单的.csproj文件,它可以用VS打开,奇妙吧?
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<OutputPath>bin\Debug</OutputPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="System">
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
2、然后再新建一个文档,输入如下代码,在和“MyProj.csproj”文件相同的路径下另存为“Program.cs”。
using System;
namespace MyProj
class Program
static void Main(string[] arg)
Console.WriteLine("Hello World!");
Console.ReadKey();
3、打开Visual Studio命令提示的DOS窗口,把当前路径设置为刚才MyProj.csproj和Program.cs两个文件所在的目录,然后命令提示符界面下输入msbuild命令,并回车,你会看到非常惊艳的一幕(下图),命令提示符窗口显示出了MSBuild构建项目的全过程。通过分析该过程,你会发现更多的细节,比如:MSBuild会首先创建“bin”、“obj”两个目录,然后执行核心的编译过程(即调用csc),最后把可执行程序以及调试符号文件从obj目录拷贝到bin目录。从这里我们也可以看到,obj目录是一个存放临时生成结果的地方,这些临时文件最终还是要链接合并到bin目录下的。
上图中我们注意看一下CoreCompile这一部分的内容,这是Msbuild调用CSharp编译器csc.exe对源文件进行编译的语句。下面我们简要对编译器csc的用法进行说明。
在Visual Studio命令提示窗口中输入【csc /?】命令,系统会列出CShart编译器的各种选项,比较常用的有:
- 【/out:<文件>】指定输出文件名
- 【/target:exe】生成控制台可执行程序
- 【/target:winexe】生成Windows可执行程序
- 【/target:library】生成类库
- 【/reference:<文件>】添加程序集引用
我们在第五章“程序集、模块、类型(Assembly/Module/Type)”中,用一个实例介绍了如何使用csc编译器进行手工编译,这里不再赘述了。
反过来,我们在VS中对项目生成进行相关配置,实际上就是在更改.csproj文件的内容。打开项目的属性页(下图),这个页面中的设置其实就是对应着.csproj文件的部分内容。例如,属性页中的“程序集名称”对应的是【SlnSuoCsprojStudy】,“输出类型”对应的是【Exe】,等等。
VS中sln、suo以及csproj文件探秘
有些事情,越是经常遇到,反而会对他们熟视无睹,只有痛过才会懂得珍惜。大家先喝一碗鸡汤,呵呵!在.NET项目的文件目录中,我们经常会看到sln、suo以及csproj文件的身影,但由于是否理解它们的意义并不会影响我们的开发,所以会熟视无睹。
下面我们就对这些熟悉又陌生的身影进行探究。
其实这些文件都是用来描述者解决方案或项目中都包含哪些文件、如何组织,以及如何编译构建的。
1、sln(Microsoft Visual Studio Solution):解决方案文件,主要用来记录当前解决方案中有哪些项目,以及一些全局性平台配置。
2、suo(Visual Studio Solution User Options):解决方案的用户选项,是一个二进制文件,用来记录用户在操作VS过程中需要记录的一些差异化配置。
不知道大家有没有注意到这样一些现象:
在关闭VS之后,再重新打开该项目,会发现VS会自动恢复到关闭前打开的那个文档,好像VS能够记住关闭前我们的工作做到了那一步,非常人性化;还有就是,当一个解决方案中有多个项目的时候,我们往往把其中某一个项目设置为“启动项目”,关闭VS后再打开,该项目仍是启动项目。
这些都是suo文件的功劳。即使我们把suo文件删掉,VS仍会自动为我们创建一个。
3、csproj(Visual C# Project file):CSharp项目文件,本质上是MSBuild的格式化文件,在第七章“Msbuild(构建)”中详细说明了该文件的意义,此处略过。
程序集签名
在VS中任意打开一个项目,在“解决方案管理器”中右键点击某个项目,在弹出的快捷菜单中单击“属性”,在出现的界面中切换到“签名”选项卡。在这个选项卡中,你会看到最下面有一个“为程序集签名”的复选框、一个“选择强名称密钥文件”的下拉列表,以及一个“仅延迟签名”的复选框。这些是干什么用的呢?
这样做的目的是给我们的程序集进行“数字签名”,所谓的“数字签名”就相当于,我们要给某人寄一封信,在信的末尾签署上您的名字,收信人可以通过验证您的签名来判断这封信是否您本人写的。这个比喻不够完备,其实数字签名技术除了可以验证发送方的身份之外,还有两个重要的功能,一个是数据的完整性验证,另一个是不可抵赖性。通过验证数字签名,收件人可以判断出数据是否被他人篡改;同时,发件人也不能否认确实是自己发送的这些数据。
程序集除了本机运行之外,还有更多的程序集是作为类库或框架供他人使用的,比如微软的mscorlib.dll,我们用到的.NET所有的基础类(Object、String、Console)都是这个程序集中的类型。假如有人私自修改了mscorlib.dll这个文件的数据,甚至是添加了病毒代码,然后把篡改过的mscorlib.dll挂到网上,而你不幸下载到的是这个“盗版”程序集,岂不是很悲催?
不过你大可不必担心,“程序集签名”正是解决这一问题的,如果你私自修改经过签名的程序集,使用者可以通过验证程序集的签名,从而判断该程序集有没有被非法篡改。
不信的话,我们做个实验。随便找一个ActiveX控件,即扩展名为.ocx的文件。鼠标右键点击该文件,选择“属性”菜单,在“属性”窗口中选择“数字签名”标签页(图),可以看到该文件的数字签名。在“签名列表”中选中一个项目,然后点击“详细信息”,弹出(图)所示的对话框,该对话框中显示“此数字签名正常”。
如果我们用UltraEdit之类的二进制编辑器对该ocx文件的内容进行一些修改并保存,再用刚才的方法查看其数字签名信息,会发现(图)显示“此数字签名无效”。怎么样?你对文件的非法篡改被识破了。
我们在VS中对我们自己的程序集进行签名,为的也是这个目的。下面谈一下数字签名的原理,以及CLR是如何对程序集的数字签名进行验证的。
要想搞明白数字签名,先要了解加密的概念。
加密的用途分为两类,一是保护数据内容不被非法查看,二是确保数据的完整性、身份合法性及不可抵赖性。我们通常所理解的加密指的是第一种用途,比如把电子邮件的内容进行加密处理,即使在传递过程中被人截获,也不能查看邮件的内容。至于第二种用途,就是我们所说的“数字签名”。
加密的方式分为私钥加密和公钥加密,又称对称加密和非对称加密。私钥加密是指用来加密和解密的密钥是同一个,所以又称为对称加密;而公钥加密用来加密和解密的密钥不是同一个,即非对称加密。
给程序集进行数字签名用的是公钥加密方式,签名及验证的具体原理(图)是:
1、 程序集发布者首先对程序集PE文件本身进行哈希处理,得到一个哈希值(数据指纹)
2、 程序集发布者用自己的私钥对该哈希值(数据指纹)进行加密,进而得到一个RSA数字签名。
3、 把RSA数字签名嵌入到程序集的PE文件。
4、 程序集发布者把自己的公钥嵌入到程序集的PE文件,此时程序集中已经包含了发布者的公钥以及用私钥加密的数字签名。
5、 程序集的使用者在引用该程序集时,先从程序集元数据中取得发布者的公钥。
6、 程序集的使用者用发布者的公钥对程序集CLR头中的RSA数字签名进行解密,得到一段数据A。
7、 程序集的使用者对程序集PE文件本身进行哈希处理,得到哈希值数据B。
8、 程序集的使用者对数据A和B进行对比,若二者相同,则说明程序集没有被篡改,反之则说明该程序集已经被非法篡改。
VS项目属性页的“签名”标签页的下面,有一个“仅延迟签名”的复选框,这是干什么用的呢?
我们知道,私钥属于公司的绝密范畴,不可能让开发人员随便接触到,甚至会把一个私钥分成若干数据段,由不同的人保管,每个人都不知道私钥的全部。所以,为了方便开发人员日常的编码调试工作,微软提出了“延迟签名”的概念,就是说先不给程序集签名,在PE文件结构中保留数字签名的物理空间,待程序集要真正发布时再统一签名。延迟签名后,程序集不能被运行,也不能进行调试。
foreach关键字与IEnumeratable
对于foreach关键字,相信靠.NET吃饭的小伙伴们肯定不会陌生,它的作用是遍历某个集合内的所有元素,效果上大致相当于for循环(注意:只能说是大致相当于,二者是不同的)。由于foreach关键字如此常用,致使人们对它习以为常,以为它就是一个for循环的简装版。
foreach只是.NET给我们的一块语法糖,使我们可以用简便的方式来处理集合中的元素,只有编译器知道正在发生了什么。下面以一个例子来分析foreach背后的真相。新建一个控制台项目ForeachStudy,敲入如下代码,生成项目,得到ForeachStudy.exe;打开VS自带的反汇编工具ILDasm(图),并用该工具打开刚才得到的ForeachStudy.exe(图),找到入口Main()函数的反编译结果(图)。
class Program
static void Main(string[] args)
List<string> list = new List<string> { "Lily","Tom","Jim" };
foreach (var l in list)
在Main函数的反编译代码中,我们并没有找到foreach关键字(当然,IL中确实也不存在foreach),但可以看到类似于::GetEnumerator()、::get_Current()、::MoveNext()等语句,这些是什么呢?
原来,GetEnumerator()方法是接口IEnumerable的成员,get_Current()、MoveNext()是接口IEnumerator的成员。IEnumerable和IEnumerator这两个接口名称有点像,大家要注意区分二者。本例中用到的List对象之所以能用foreach实现遍历,究其原因,就是因为该类实现了IEnumerable接口。不信的话,可以把光标定位到List,按下F12键,进入到该类型的定义元数据,会看到如下的方法声明。
public class List<T> : IList<T>, ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable
每当编译器遇到foreach关键字的时候,它就会执行如下操作:
1、 试图把foreach操作的对象转换为IEnumerable接口的实例;
2、 如果对象并没有实现IEnumerable接口,则转换失败,编译器会报错;
3、 如果对象实现了IEnumerable接口,则会首先调用对象的GetEnumerator()方法,得到一个枚举器对象;
4、 调用该枚举器对象的MoveNext()方法,从而访问下一个位置;
5、 访问枚举器对象的Current属性得到当前位置的元素;
6、 重复第4步,直到MoveNext()方法返回false。
上述步骤用代码表示如下:
List<int> list = new List<int> { 1, 2, 3 };
var enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
int j = enumerator.Current;
在第2步中,如果编译器对操作对象到IEnumerable接口的转换失败,编译器会把foreach关键字划上蓝色的波浪线,提示我们出现了编译时错误。此时如果试图编译程序,VS会提示错误(图):【“int”不包含“GetEnumerator”的公共定义,因此 foreach 语句不能作用于“int”类型的变量】。这再一次印证了编译器试图把foreach操作对象理解为实现了IEnumerable接口的对象。
表达式树及访问者模式、双分派
熟悉GOF常用设计模式的同学肯定知道,访问者模式是23种设计模式中最为复杂、最难理解的一种,该模式的目的是解除数据和操作之间的耦合关系,适用于数据结构类型相对固定、对数据结构的操作多变的情形。对该模式不太熟悉的朋友请自行查询相关资料,这里不做过多说明,下图是访问者模式的UML类图。
下面重点说一下访问者模式中的一个重要概念,即“双分派”机制。
相信不少同学在学习访问者模式的时候,被这个所谓的“双分派”搞的云里雾里?我们用下面的代码作为示例来说明:
public abstract class ComputerPart
public string Name { set; get; }
public abstract void Accept(Visitor visitor);
public class Mouse : ComputerPart
public override void Accept(Visitor visitor)
visitor.Visit(this);
public class Keyboard : ComputerPart
public override void Accept(Visitor visitor)
visitor.Visit(this);
public class Computer : ComputerPart
public List<ComputerPart> Parts = new List<ComputerPart>();
public override void Accept(Visitor visitor)
foreach (var p in Parts)
p.Accept(visitor);
public abstract class Visitor
public void Visit(ComputerPart part)
throw new NotImplementedException();
public virtual void Visit(Mouse mouse)
mouse.Accept(this);
public virtual void Visit(Keyboard keyboard)
keyboard.Accept(this);
public class GetInfomationVisitor : Visitor
public override void Visit(Keyboard keyboard)
string r = string.Format("设备名称:{0}", keyboard.Name);
Console.WriteLine(r);
public class RepairVisitor : Visitor
public override void Visit(Mouse mouse)
string r = string.Format("修理了{0}", mouse.Name);
Console.WriteLine(r);
public override void Visit(Keyboard keyboard)
string r = string.Format("修理了{0}", keyboard.Name);
Console.WriteLine(r);
public class Client
public void Main()
ComputerPart p = new Keyboard();//变量p声明为ComputerPart类型,实际上却赋值为Keyboard类型
Visitor v = new RepairVisitor();
* 1、方法重载
* 2、继承多态
v.Visit(p);//在方法重载中,实际调用那个方法,取决于参数的声明类型。此处,虽然变量p的实际类型是Keyboard,但其声明类型是ComputerPart,所以仍会调用形参类型是ComputerPart的重载版本。
p.Accept(v);//而在继承多态中,虽然变量p声明为ComputerPart类型,但其实际类型是Keyboard,所以会调用Keyboard类的Accept方法。在Keyboard类的Accept方法中,再调用visitor.Visit(this)方法,此时作为实参的this已经是Keyboard类了,所以能够调用正确的重载版本,这就是所谓的双分派机制
关于表达式树这个概念,想必有的同学会感到陌生。表达式树,顾名思义,是用一个树状结构来表示表达式,它体现了一个“代码即数据”的概念。下图就是一个表达式树,它用一个类似二叉树的数据结构,表示了一个加减乘混合运算。
(1+2)*(5-3)
而下面的代码则用程序语言表达出了上面那个表达式树,其中的变量g就是最终的表达式变量,它就代表了上面的那个加减乘混合运算。
var a = Expression.Constant(1);
var b = Expression.Constant(2);
var c = Expression.Add(a, b);
var d = Expression.Constant(5);
var e = Expression.Constant(3);
var f = Expression.Subtract(d, e);
var g = Expression.Multiply(c, f);
对于表达式树的遍历、访问以及修改,就是借助于访问者模式来进行的,下面举例说明。
public class MyExpressionVisitor1 : ExpressionVisitor
protected override Expression VisitBinary(BinaryExpression node)
var left = Visit(node.Left);
var right = Visit(node.Right);
if (node.NodeType == ExpressionType.Multiply)
return Expression.MakeBinary(ExpressionType.Divide, left, right);
return base.VisitBinary(node);
public class MyExpressionVisitor2 : ExpressionVisitor
protected override Expression VisitBinary(BinaryExpression node)
var left = node.Left;
var right = node.Right;
if (node.NodeType == ExpressionType.Multiply)
return Expression.MakeBinary(ExpressionType.Divide, left, right);
return base.VisitBinary(node);
public class ClientExpressionTree
public void Main()
var a = Expression.Constant(1);
var b = Expression.Constant(2);
var c = Expression.Add(a, b);
//var c = Expression.Multiply(a, b);
var d = Expression.Constant(5);
var e = Expression.Constant(3);
var f = Expression.Subtract(d, e);
var g = Expression.Multiply(c, f);
* 当表达式为【(1+2)*(5-3)】时,r1和r2是相同的。
* 有的人肯定会问,既然r1和r2相同,那MyExpressionVisitor1和MyExpressionVisitor2就没有区别了?
* 既然r1和r2相同,那么语句【var left = Visit(node.Left);】和【var left = node.Left;】中,为什么一个调用了Visit方法,另一个没有调用呢?
* 答案是否定的,二者是有区别的。
* 我们把变量c的定义从加法改为乘法,再次运行程序,会发现,此时的r1和r2不相同了。
* 这是为什么呢?
* 原因是【var left = Visit(node.Left);】语句发生了递归调用,而【var left = node.Left;】语句没有递归。
var visitor1 = new MyExpressionVisitor1();
var r1 = visitor1.Visit
(g);
var visitor2 = new MyExpressionVisitor2();
var r2 = visitor2.Visit(g);
下面再举一个例子,加深对表达式的理解。
DateTime d = DateTime.Now;
var instance = Expression.Constant(d);
var param = Expression.Parameter(typeof(double), "day");
var method = typeof(DateTime).GetMethod("AddDays");
var c = Expression.Call(instance, method, param);
var lamda = Expression.Lambda<Func<double, DateTime>>(c, param);
var r = lamda.Compile()(1);
XML注释原理
相信大家都给代码添加过注释,C#中的注释有三种方式:
1、//
2、/**/
3、///
前两种也适用于C、C++等传统C系列语言的注释,而第三种是C#引入的新的注释方式(见如下代码)。这种注释方式的好处是可以被VS的智能感知系统(Intellisense)所利用,可以在敲代码的过程中给我们提供智能提示,例如,当鼠标悬放在代码上的时候,会出现一个提示气泡(图)。
/// <summary>
/// 测试类
/// </summary>
public class MyLib
/// <summary>
/// 我的方法
/// </summary>
/// <param name="i">参数</param>
/// <returns>返回值</returns>
public string MyFunc(int i)
return "";
这种注释方式是基于XML文档格式,有两种使用方式:
一是直接在代码里用“///”进行注释并编译;
二是先在代码里用“///”进行注释,然后使用csc /doc指令进行编译,得到一个程序集文件和一个xml文件。
使用如下指令进行编译,你将得到两个文件,一个是MyLib.dll,另一个是MyLib.xml。
csc /target:library MyLib.cs /doc:MyLib.xml
这里注意,若要将生成的.xml文件用于Intellisense功能,请使该.xml文件的文件名与要支持的程序集同名,然后确保该.xml文件放入与该程序集相同的目录中。这样,在Visual Studio项目中引用程序集时,也可找到该.xml文件。
使用/doc进行编译时,编译器将在源代码中搜索所有的XML标记,并创建一个XML文档文件。XML doc注释不是元数据;它们不包括在编译的程序集中,因此无法通过反射对其进行访问。
另外,在使用XML注释时,相信大家都注意到一个十分贴心的现象,就是当我们在类型或者成员上方连续敲入三个“/”时,VS会自动生成一段注释模版供我们使用或修改,这也是Intellisence提供的功能。
有的人肯定会有意无意的注意到Intellisence自动完成的注释中,有一些貌似预置好的标记,如
、、等。通过理解这些标记的英文字面意思,大概可以猜测出它们分别代表什么意思。没错,VS内部确实预先内置了一些常用的标记,当我们在XML注释区域敲入“<”符号时,Intellisence会自动弹出一个可供选择的标记列表(如下图)。使用这些预置的XML标记,我们可以更加全面的对代码进行描述。
运行时创建类型
如果问大家会不会新建一个类,大家肯定会说“当然会啦”,不就是像下图所示的那样创建吗?在IDE中新建一个类,给这个类添加代码,保存到磁盘,然后在别的地方使用这个类。没错,这就是我们通常在VS中创建类的方式,类是在编译前静态创建的。
接下来再问大家一个问题,如果事先没有在IDE的代码编辑器中创建类,能不能在程序运行期间动态的创建一个类呢?这个类不存在于物理磁盘上,没有对应的物理文件,只存在于内存中。这个问题的答案就是TypeBuilder。
/// <summary>
/// 要动态构造的目标类型
/// </summary>
public class MyDynamicClass
private int numberFiled;
public MyDynamicClass(int n)
numberFiled = n;
public void MyFunc(string str)
Console.WriteLine(str);
public int NumberField
return numberFiled;
public class TypeBuilderCase
public TypeBuilderCase()
#region 定义程序集
AssemblyName aName = new AssemblyName("MyDynamicAssembly");
AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(aName, AssemblyBuilderAccess.RunAndSave);
#endregion
#region 定义模块
ModuleBuilder mb = ab.DefineDynamicModule(aName.Name, aName.Name + ".dll");
#endregion
#region 定义类型
TypeBuilder tb = mb.DefineType("MyDynamicClass", TypeAttributes.Public);
#endregion
#region 定义字段
FieldBuilder numberFiledBuilder = tb.DefineField("numberFiled", typeof(int), FieldAttributes.Private);
#endregion
#region 定义属性
PropertyBuilder pb = tb.DefineProperty("NumberField", PropertyAttributes.None, typeof(int), null);
MethodBuilder getNumberFieldMethodBuilder = tb.DefineMethod("get_NumberField", MethodAttributes.Public, typeof(int), null);
ILGenerator getNumberFieldMethodIL = getNumberFieldMethodBuilder.GetILGenerator();
getNumberFieldMethodIL.Emit(OpCodes.Ldarg_0);
getNumberFieldMethodIL.Emit(OpCodes.Ldfld, numberFiledBuilder);//把numberFiledBuilder的值压入堆栈
getNumberFieldMethodIL.Emit(OpCodes.Ret);
pb.SetGetMethod(getNumberFieldMethodBuilder);
#endregion
#region 定义构造方法
ConstructorBuilder ctorBuilder = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(int) });
ILGenerator ctorIL = ctorBuilder.GetILGenerator();
ctorIL.Emit(OpCodes.Ldarg_0);
ctorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));//调用父类的构造方法
ctorIL.Emit(OpCodes.Ldarg_0);
ctorIL.Emit(OpCodes.Ldarg_1);
ctorIL.Emit(OpCodes.Stfld, numberFiledBuilder);//把堆栈顶端的值赋给numberFiledBuilder
ctorIL.Emit(OpCodes.Ret);
#endregion
#region 定义普通方法
MethodBuilder myFuncMethodBuilder = tb.DefineMethod("MyFunc", MethodAttributes.Public, typeof(Nullable), new Type[] { typeof(string) });
ILGenerator myFuncMethodIL = myFuncMethodBuilder.GetILGenerator();
myFuncMethodIL.Emit(OpCodes.Ldarg_0);
myFuncMethodIL.Emit(OpCodes.Ldarg_1);
myFuncMethodIL.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
myFuncMethodIL.Emit(OpCodes.Ret);
#endregion
Type myClass = tb.CreateType();//完成类型的创建工作
ab.Save(aName.Name + ".dll");//把程序集保存到物理磁盘
var myInstance = Activator.CreateInstance(myClass, 1);//利用反射进行实例化
#region 利用反射调用实例的方法
MethodInfo myFuncMethod = myClass.GetMethod("MyFunc");
myFuncMethod.Invoke(myInstance, new object[] { "Hello" });
#endregion
#region 利用反射访问实例的属性
PropertyInfo numberFieldProperty = myClass.GetProperty("NumberField");
var number = numberFieldProperty.GetValue(myInstance, null);
#endregion
#region 利用反射获取实例的私有字段
FieldInfo numberField = myClass.GetField("numberFiled", BindingFlags.NonPublic | BindingFlags.Instance);
var n = numberField.GetValue(myInstance);