C# 程序集
一、概述
-
程序集是.NET应用程序的部署单元
程序集是.NET应用程序的部署单元。
程序集是自我描述的安装单元,由一个或多个文件组成。
通常扩展名是EXE或DLL的.NET可执行程序称为程序集。
.NET程序集包含元数据。 - 程序集的特性
- 程序集是自我描述的。
- 版本的相互依赖性在程序集的清单中记录。
- 程序集可以并行加载。
- 应用程序使用应用程序域来确保其独立性。
- 安装简单。
-
程序集的结构
程序集由描述它的元数据、描述导出类型和方法的类型元数据、MSIL代码和资源组成。
- 程序集的清单
程序集清单是元数据的一部分,描述了程序集和引用它所需要的所有信息和依赖关系。- 标识(名称、版本、文化和公钥)。
- 属于该程序集的一个文件列表。
- 引用程序集的列表。
- 一组许可请求——运行这个程序集所需要的许可。
- 导出的类型。
-
命名空间和程序集
命名空间完全独立于程序集。
在一个程序集中可以有不同的命名空间,一个命名空间也可以分布在多个程序集中。
命名空间是类名的一种扩展,属于类名范畴。 -
私有和共享程序集
私有程序集在应用程序所在目录下或子目录中。
私有程序集需要注意命名冲突。
使用共享程序集时,需要注意:程序集必须是唯一的,有强名(唯一的名称,强制的版本号) -
辅助程序集
辅助程序集是只包含资源的程序集,它尤其适用于本地化。 -
查看程序集
ildasm
,这是一个MSIL反汇编程序。命令行运行ildasm
,把程序集作为其参数或File
|Open
菜单。
还有.NET Reflector是用于分析程序集的工具。
二、构建程序集
-
创建模块和程序集
模块是一个没有程序集特性的DLL。
csc /target:module hello.cs
创建模块hello.netmodule
ildasm hello.netmodule
查看这个模块。/addmodule
选项,可以把模块添加到现有的程序集中。创建一个
A.cs
类文件,然后csc /target:module A.cs
编译,生成了A.netmodule
文件,它不包括程序集的信息;下面生成一个程序集B,它包括模块A.netmodule
,命令如下:csc /target:library /addmodule:A.netmodule /out:B.dll
,然后使用IL
查看程序集:ildasm B.dll
;如图所示:在使用IL查看程序集时,只能找到一个清单。
模块的作用是更快地启动程序集,因为不是所有类都在一个文件中,模块只在需要时加载。另外可以使程序集可以使用多种编程语言来创建。
- 程序集的属性
./Solution Explorer/Properties/AssemblyInfo.cs
文件,可以使用一般的源代码编辑器配置程序集的属性。assembly
前缀把属性标记为全局属性。- 程序集的全局属性与特定的语言元素无关,用于程序集属性的参数是命名空间
System.Reflection
、System.Runtime.Compiler Services
、System.Runtime.InteropServices
中的类。 System.Reflection
命名空间中定义的程序集属性列表:
在VS中,可以右键项目 - 属性 - 应用程序设置 - 程序集信息 来配置这些属性: -
动态加载和创建程序集
在开发期间,添加对程序集的引用,该程序集中的类型就可用于编译器。
可以变成加载程序集。为此可以使用类Assembly
的静态方法Load()
,这个方法是重载的,可以使用AssemblyName
给它传送程序集的名称或字节数组。
例子:使用WPF,输入代码,点击按钮执行并将结果显示到TextBlock中。创建类
CodeProvider
,定义方法CompileAndRun()
,编译文本框中的代码,启动所生成的方法。代码如下:using System; using System.CodeDom.Compiler; using System.IO; using System.Reflection; using System.Text; using Microsoft.CSharp; namespace 动态加载和创建程序集 { public class CodeProvider { private string prefix = "using System;" + "public static class Driver" + "{" + "public static void Run()" + "{"; private string postfix = "}" + "}"; public string CompileAndRun(string input, out bool hasError) { hasError = false; string returnData = null; CompilerResults results = null; using (CSharpCodeProvider provider = new CSharpCodeProvider()) { CompilerParameters options = new CompilerParameters(); options.GenerateInMemory = true; StringBuilder sb = new StringBuilder(); sb.Append(prefix); sb.Append(input); sb.Append(postfix); results = provider.CompileAssemblyFromSource( options, sb.ToString()); } if(results.Errors.HasErrors) { hasError = true; StringBuilder errorMessage = new StringBuilder(); foreach (CompilerError error in results.Errors) { errorMessage.AppendFormat("{0} {1}", error.Line, error.ErrorText); } returnData = errorMessage.ToString(); } else { TextWriter temp = Console.Out; StringWriter writer = new StringWriter(); Console.SetOut(writer); Type driverType = results.CompiledAssembly.GetType("Driver"); driverType.InvokeMember("Run", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, null); Console.SetOut(temp); returnData = writer.ToString(); } return returnData; } } }
按钮的
Click
事件连接到Compile_Click
方法上:实例化CodeProvider
类,调用方法CompileAndRun()
;从文本框textCode中提取输入,把结果写到TextBlock控件textOutput中:private void Compile_Click(object sender, RoutedEventArgs e) { CodeProvider driver = new CodeProvider(); bool isError; textOutput.Text = driver.CompileAndRun(textCode.Text, out isError); if(isError) { textOutput.Background = Brushes.Red; } }
程序运行结果如图所示:
-
应用程序域
.NET的应用程序边界:应用程序域。使用托管IL代码,运行库就不能访问同一个进程中的另一个应用程序的内存。多个应用程序可以运行在一个进程的多个应用程序域中。
AppDomain
类用于创建和中断应用程序域,加载和卸载程序集和类型、枚举域中的程序集和线程。例子:创建控制台程序
AssemblyA
,在Main()
方法中添加Console.WriteLine()
,再添加一个类Demo
,其构造函数的参数是两个int
值,用AppDomain
类创建实例。namespace AssemblyA { class Program { static void Main(string[] args) { Console.WriteLine("Main in domain {0} called", AppDomain.CurrentDomain.FriendlyName); ; Console.ReadKey(); } } }
namespace AssemblyA { public class Demo { public Demo(int val1, int val2) { Console.WriteLine("Constructor with the values {0}, {1}" + " in domain {2} called", val1, val2, AppDomain.CurrentDomain.FriendlyName); } } }
运行结果如下图所示:
接着创建控制台程序
DomainTest
,首先使用AppDomain
类的FriendlyName
属性显示当前域的名称;调用CreateDomain()
方法,创建一个新的应用程序域New AppDomain
,然后把程序集AssemblyA
加载到新域中,通过调用ExecuteAssembly()
来调用Main()
方法:namespace DomainTest { class Program { static void Main(string[] args) { AppDomain currentDomain = AppDomain.CurrentDomain; Console.WriteLine(currentDomain.FriendlyName); AppDomain secondDomain = AppDomain.CreateDomain("New AppDomain"); secondDomain.ExecuteAssembly("AssemblyA.exe"); /*secondDomain.CreateInstance( "AssemblyA", "AssemblyA.Demo", true, System.Reflection.BindingFlags.CreateInstance, null, new object[] { 7, 3 }, null, null, null);*/ Console.ReadKey(); } } }
启动程序前先把
Assembly.exe
复制到当前项目目录下./bin/Debug
,运行结果:看到第二行
New AppDomain
中新加载的程序集的输出结果里看不到AssemblyA.exe
的执行,因为没有创建新的进程;用CreateInstance()
替代ExecuteAssembly()
方法:它的第一个参数是程序集名称,第二个参数定义了应实例化的类,第三个参数true
表示不区分大小写,System.Reflection.BindingFlags.CreateInstance
是一个绑定标志枚举值,指定应调用的构造函数;只需要将上面代码secondDomain.ExecuteAssembly("AssemblyA.exe");
注释掉,下面/**/
中的代码取消注释,运行结果如下:
在运行期间,主应用程序域会自动创建。ASP.NET为每个运行在Web服务器上的Web应用程序创建一个应用程序域;Internet Explorer创建运行托管控件的应用程序域;对于应用程序,卸载程序集只能通过中断应用程序域来进行。如果程序集是动态加载的,且需要在使用完后卸载程序集,应用程序域就是非常有用的。在主程序域中,不能删除已加载的程序集,但可以终止应用程序域,在该应用程序域中加载的所有程序集都会从内存中清除出去。
修改之前的WPF程序:添加一个新类,使用
CreateInstanceAndUnwrap()
实例化类CodeDriver
,调用CompileAndRun()
方法,之后再次卸载新应用程序域。namespace 动态加载和创建程序集 { public class CodeDriverInAppDomain { public string CompileAndRun(string code, out bool hasError) { AppDomain codeDomain = AppDomain.CreateDomain("CodeDriver"); CodeDriver codeDriver = (CodeDriver) codeDomain.CreateInstanceAndUnwrap( "动态加载和创建程序集", "动态加载和创建程序集.CodeDriver"); string result = codeDriver.CompileAndRun(code, out hasError); AppDomain.Unload(codeDomain); return result; } } }
要在另一个应用程序域中访问类
CodeDriver
,类CodeDriver
就必须派生于基类MarshalByRefObject
。所以CodeDriver.cs
修改:namespace 动态加载和创建程序集 { public class CodeDriver : MarshalByRefObject { ... }
按钮事件处理程序可以修改为使用新类
CodeDriverInAppDomain
:private void Compile_Click(object sender, RoutedEventArgs e) { //CodeDriver driver = new CodeDriver(); CodeDriverInAppDomain driver = new CodeDriverInAppDomain(); bool isError; textOutput.Text = driver.CompileAndRun(textCode.Text, out isError); if(isError) { textOutput.Background = Brushes.Red; } }
使用
AppDomain
类的GetAssemblies()
方法,可以查看应用程序域中加载的程序集。 - 共享程序集
共享程序集必须有一个强名
共享程序集必须有一个强名,来唯一地标识该程序集。
强名由以下组成:- 程序集本身的名称
- 版本号
- 公钥
- 文化
为了唯一地标识公司中的程序集,应使用命名空间层次结构来给类命名。
查看全局程序集缓存:在`C:\Windows\assembly\ ` 目录下 。
程序集查看器可以使用Windows资源管理器查看和删除程序集。`gacutil.exe`工具可以使用命令行安装、卸载和显示程序集。
**创建共享程序集**
建立一个Visual C# Class Library
项目SharedDemo
;命名空间Wrox.ProCSharp.Assemblies.Sharing
,类名SharedDemo
。
类的构造函数把文件的所有行都将读取到一个集合中;文件名作为参数传送给构造函数;方法GetQuoteOfTheDay()
只返回这个集合的一个随机字符串。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
namespace Wrox.ProCSharp.Assemblies.Sharing
{
public class SharedDemo
{
private List<string> quotes;
private Random random;
public SharedDemo(string filename)
{
quotes = new List<string>();
Stream stream = File.OpenRead(filename);
StreamReader streamReader = new StreamReader(stream);
string quote;
while ((quote = streamReader.ReadLine()) != null)
{
quotes.Add(quote);
}
streamReader.Close();
stream.Close();
random = new Random();
}
public string GetQuoteOfTheDay()
{
int index = random.Next(1, quotes.Count);
return quotes[index];
}
}
}
要共享这个程序集,需要一个强名,使用强名工具(sn):sn -k mykey.snk
,强名工具生成和编写一个公钥/私钥对,并把改密钥对写到文件中。
在VS中,可以选择签名(signing)选项卡,用项目属性标记程序集。
设置了signing选项后,重新建立文件后,公钥就在程序集清单中。
**安装共享程序集**
程序集中有了公钥后就可以使用全局程序集缓存工具gacutil
及其/i
选项把它安装到全局程序集缓存中:gacutil /i SharedDemo.dll
。
用VS配置以后建立的事件命令行,就可以在全局程序集缓存中安装每个成功建立的程序集。