学习笔记---元数据、程序集、GAC版本控制、属性(Attribute)、反射(利用.NET编译器实现表达式计算器)
时间:2010-12-19 来源:RJ
NET应用程序实际上由 代码、数据和元数据组成, 元数据的来源有两个: 属性Attribute(非封装字段用的Property)和.Net编译器(主要来源). 元数据是跟程序存储在一起的(例如: //后便的注释信息在编译时是被忽略掉的, 而Attribute属性的信息是会被添加到程序集中的).
2. 简单讲, 程序集就是.dll或.exe文件. 程序集有两类: 专有程序集(Private Assemblies)和共享程序集(Shared Assemblies), 专有程序集只被一个应用程序使用; 而共享程序集可以被多个应用程序共享.
.NET编程环境中的共享程序集可以由名称和版本号唯一的标识. 使用共享程序集有一个”DLL HELL”的问题, 虽然程序集名称和版本号可以唯一确定一个程序集, 但版本号是个不可预测的值, 在编写代码时候载入的还是程序集的名称, 这样就会出现这么一种情况: 软件A和软件B 开始都用到一个名称为asm.dll(Ver 1,0), 后来A软件的新版本更新了asm.dll为ver2.0 版本. 若用户先安装B再安装A, 由于大多数程序集设计时的兼容性考虑, A和B都可以工作. 但当用户先安装A在安装B, 则B的老版本程序集会覆盖掉A的新版本程序集, 这样软件A就很可能出现错误而无法正常使用(asm.dll ver 2.0添加了新功能). 由此, 引入了一个版本控制(Versions)的问题.
3. 版本控制(Version): 全局程序集缓存(The Global Assembly Cache, GAC)允许同一个程序集的较老版本和较新版本可以同时存在. GAC采用了强名称的手段来解决DLL HELL的问题.
强名称由4个部分组成: 程序集名称、版本号、文化、公钥(数字签名), 这4个组成部分可以在任意项目的properties项的AssemblyInfo.cs文件中看到. 我们在为程序集添加强名称后可以通过类似于C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin目录中的gacutil工具添加到GAC中(命令: gacutil /i AssemblyName.dll), 也可以在添加强名称后, 直接用鼠标拖动AsseblyName.dll释放到C:\Windows\Assembly目下即可(实际上进入Assembly目录后, 文件浏览器讲自动变成GAC工具程序).
再说说, 公钥加密: 公钥加密的本质: 建立两个密钥, 被第一个密钥加密的数据只能被第二个密钥解密, 而第二个密钥加密的数据也只能被第一个密钥解密. 这两个密钥一个叫公钥, 用于向外发布, 任何人都可以得到; 而两外一个叫做私钥, 只有客户机自己知道.
加密通信: 假设服务器发送给客户机的数据, 经过客户端公钥加密后发送, 就只有客户机的私钥可以解密, 这样就实现了数据加密的功能. 但客户机如果在回发加密数据给服务器的话, 客户机需要使用服务器的公钥加密, 这样服务器就可以用自己的私钥解密, 整个过程分别使用了服务器的密钥对和客户机的密钥对. 这便是加密过程.
数字签名: 再看另外一个过程, 若客户机用自己的私钥加密数据后向外发送, 而外网任何一台主机都可以通过客户端的公钥解密数据, 这样看起来好像不能够实现保密功能, 但你有没有想过这个过程保证了数据肯定是客户端发送出来的. 不管有多少人利用公钥解密了客户端私钥加密的数据, 只要他们能解开, 就说明这些数据肯定是客户端发送的, 换句话讲, 在这个过程中, 唯一标示了数据是从客户端发送的, 就好比客户端发送文件的时候签了自己的名字证明是自己发送的. 这就是数字签名的由来.
将自己的dll放到全局程序集缓存(GAC)中的过程:
a. 添加完整的文档注释, 类型及成员均需添加且不可遗漏(类: 描述... ; 方法: 根据...; 属性: 获取或设置... ; 参数: System.xx类型, 表示...)
b. 添加完整程序集信息(通过创建空类库项目, 获得AssemblyInfo文件并修改)
主版本号: 软件版本重大变化, 如: 单机版到网络版
次要版本号: 软件功能变化, 如: 添加辅助挂机功能
内部版本号: 如: beta,final, 或同时做出几个不同方案, 最终选一个
修订号: 修改bug的次数
c. 通过类库中的强名称工具获得签名密钥(需在环境变量的Path中, 添加VS2008的SDK目录c:\program files\Microsoft SDKs\Windows\v6.0A\bin)
cmd中生成强名称(注意大小写): sn -k strongname.snk 查看强名称: sn -T xxx.dll
备注: VS2005的SDK目录为c:\program files\Microsoft Visual Studio 8\SDK\v2.0\Bin
d. 将有完整文档注释的cs文件、AssemblyInfo文件及强名称文件编译成一个dll文件和XML文档
cmd中键入命令: csc /target:library /doc:xxx.xml /keyfile:strongname.snk /out:xxx.dll AssemblyInfo.cs aaa.cs bbb.cs
也可以在VS中右击项目, 选”属性”->”签名”, 使用刚创建的*.snk文件即可
e. 将xxx.dll和xxx.xml拷贝到稳定的目录(如框架类库目录: c:\windows\Microsoft.Net\Framework\v3.5\)下
cmd中使用命令gacutil /i xxx.dll注册到GAC中 使用gacutil /u xxx.dll可以反注册; 若不需要xxx.xml, 可以简单的讲xxx.dll拖到c:\windows\Assemly文件夹中
4. Attribute是一种生成元数据的机制, 用来往程序集中添加描述信息, 即往程序集中添加元数据, 这些添加的信息会随一并保存在程序集中. 可以将Attribute理解为给C#代码添加注释用的, 这种添加注释的手段和//及/**/的区别是, attribute添加的注释会保存到程序集(.dll)文件中, 而//和/**/则会被编译器忽略.
值得注意的是: 在.NET体系(或理解为.NET系统)中, 往往通过Attribute属性影响编译效果, 如: 在Web服务中、序列化时等等. 通过Attribute添加到程序集中的信息可以通过反射读取出来.
5. 反射正是从程序或程序集中读取元数据的(如: 反射的典型应用动态提示就是读取程序集清单(清单(Manifest)是元数据的一部分), Type类是反射的核心, Type类封装了对象类型的表示. 反射的定义指某个程序读取其自身或其他程序的元数据的过程.
再来看看Type类: 刚才说过Type封装了对象类型的表示, 实际上我们每定义一个类(如: Student类)在系统中都会一一对应Type类的一个对象(如: student对象), 而系统使用的正是这个student对象. 可以简单记作: 类型 <---> Type类的对象.
Type类是访问元数据的主要手段, 并且Type类继承自MemberInfo类, MemberInfo类封装了所有类成员的相关信息.
使用typeof关键字将会为我们创建一个Type类的对象, 该Type类的对象用来代表一个类(如: Student类). 听起来好像有点绕, 关键点是 Type类的某个对象会一一对应某个类.
typeof和GetType()的区别: typeof用在已经知道的类型的情况中, 用typeof获得Type类的一个对象, 而GetType()是一个实例方法, 事先不知道该实例的类型, 在运行时才会获得该实例的类型.
读取元数据只是反射的常见任务之一, 反射常用于4种任务:
a. 查看元数据 b. 查看类型(类型发现) d. 对方法和特性的迟绑定(也叫动态激活或运行时绑定) c. 运行时创建类型
关于上边的几个概念间的关系可以参加下图:
获得元数据示例:
//Reflection
代码using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Reflection
{
class Program
{
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)] //2. 用AttributeUsage标示出标注属性的作用范围(类、方法、属性等)
public class Signature : Attribute //1. 自定义类(封装需要标注的属性), 并继承自Attribute
{
private string _author;
private string _datetime;
private string _company;
public Signature(string author)
{
this._author = author;
this._datetime = DateTime.Now.ToShortDateString();
}
public string Author
{
get
{
return this._author;
}
set
{
this._author = value;
}
}
public string Company
{
get
{
return this._company;
}
set
{
this._company = value;
}
}
public string CreateDate
{
get
{
return this._datetime;
}
}
}
//3. 使用标注属性, 这部分的注释会添加到程序集中, 而普通的注释编译时会还忽略. 属性可以跟在构造函数的后边进行赋值.
[Signature("Jalen")] //这里也可以加多个标注属性
[Signature("Jalen", Company = "CAIT")]
//[Distribution("北京")]
public class Student
{
string _name;
string _phone;
public Student(string name, string phone)
{
this._name = name;
this._phone = phone;
}
public string Name
{
get
{
return this._name;
}
set
{
this._name = value;
}
}
public string Phone
{
get
{
return this._phone;
}
set
{
this._phone = value;
}
}
}
static void Main(string[] args)
{
Student s = new Student("Alice", "1388888888");
Console.WriteLine("姓名: {0} , 电话: {1}\n",s.Name,s.Phone);
Console.WriteLine("---------------------------------------");
Type type = typeof(Student); //每个类对应着Type类的一个对象(一一对应), typeof的结果获得一个type类的对象
System.Reflection.MemberInfo mi = type; //MemberInfo类是Type类的父类(多肽)
object[] obj = mi.GetCustomAttributes(typeof(Signature), false); //用GetCustomAttributes获得标注属性,这里object[]实际上就是Signature[]
for (int i = 0; i < obj.Length; i++)
{
Signature sig = obj[i] as Signature;
Console.WriteLine("作者: {0} , 创建时间: {1} , 版权所属: {1} \n", sig.Author, sig.CreateDate, sig.Company);
}
}
}
}
//方法的迟绑定示例
//DynamicCallMethod
代码using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DynamicCallMethod
{
public class AddMehod
{
public double add(int a, int b)
{
return Convert.ToDouble(a + b);
}
}
class Program
{
static void Main(string[] args)
{
//获得Type对象, 该对象供系统用, 与System.Math类一一对应.
Type mathtype = Type.GetType("System.Math");
//Type mathtype = typeof(System.Math);
//保存参数类型的数组, 参数类型double也是Type的一个对象
Type[] paramTypes = new Type[1];
paramTypes[0] = Type.GetType("System.Double");
//获得Cos()方法的信息
System.Reflection.MethodInfo cosinfo = mathtype.GetMethod("Cos", paramTypes);
//保存实际参数的数组, 若无参数则可以创建长度为0的数组(new Object[0])
Object[] param = new Object[1];
param[0] = 45 * (Math.PI / 180); //求45度角对应的弧度
Object returnvalue = cosinfo.Invoke(mathtype, param); //cosinfo是方法, mathtype是类名, param是实际参数
Console.WriteLine("弧度是{0}度",returnvalue);
}
}
}
//运行时创建类型示例(利用.NET编译过程的计算器)
// CalculatorBLL.cs
代码using System;
using System.Collections.Generic;
using System.Text;
namespace BLL
{
public class CalculatorBLL
{
#region 利用堆栈实现计算器
public double Compute(string expression)
{
//处理最开始的-或者+号
if (expression.StartsWith("+") || expression.StartsWith("-"))
{
expression = "0" + expression;
}
//1. 将+、-、*、/分离成出来
expression = expression.Replace("+", ",+,");
expression = expression.Replace("-", ",-,");
expression = expression.Replace("*", ",*,");
expression = expression.Replace("/", ",/,");
string[] infos = expression.Split(',');
Stack<string> stack = new Stack<string>(); //使用list模拟堆栈
//2. 利用堆栈计算乘除的结果
double prenum;
double nextnum;
for (int i = 0; i < infos.Length; i++)
{
if (infos[i] == "*" || infos[i] == "/")
{
prenum = Convert.ToInt32(stack.Pop());
nextnum = Convert.ToInt32(infos[i + 1]);
if (infos[i] == "*")
{
stack.Push((prenum * nextnum).ToString());
}
else
{
stack.Push((prenum / nextnum).ToString());
}
i++; //别忘了i要++
}
else
{
stack.Push(infos[i]);
}
}
//3. 利用堆栈计算加减的结果
//infos = stack.ToArray(); //将stack转存到数组中, 这里转存的结果是逆序的
infos = new string[stack.Count];
for (int i = infos.Length-1; i >= 0; i--) //将stack正序转存到数组中
{
infos[i] = stack.Pop();
}
prenum = 0; //重置prenum和nextnum
nextnum = 0;
for (int i = 0; i < infos.Length; i++)
{
if (infos[i] == "+" || infos[i] == "-")
{
prenum = Convert.ToInt32(stack.Pop());
nextnum = Convert.ToInt32(infos[i + 1]);
if (infos[i] == "+")
{
stack.Push((prenum + nextnum).ToString());
}
else
{
stack.Push((prenum - nextnum).ToString());
}
i++; //别忘了i++
}
else
{
stack.Push(infos[i]);
}
}
return Convert.ToInt32(stack.Pop());
}
#endregion
#region 利用.NET的编译过程实现计算器
/*.NET的编译器在编译过程中就将常量表达式计算出来, 但是编译过程只有一次, 而我们希望多次计算(每点一次按钮就编译一次).
因此为了得到一次结果就必须执行一次编译过程, 我们可以用动态拼接字符串的方法拼一个类, 然后调用Process.start执行csc得到编译结果*/
public double Calculate(string expression)
{
GenerateTempClass(expression); //生成临时类
#region 通过反射加在程序集并调用其中的方法, 缺点: 只能执行一次
/*
System.Reflection.Assembly asm = System.Reflection.Assembly.LoadFrom(GetCurrentPath() + @"\TempClass.dll");
ITempClass itc = asm.CreateInstance("TempClass") as ITempClass;
return itc.TempMethod();
*/
#endregion
#region
/*通过反射获得刚编译的程序集, .NET只提供了加载程序集的功能而无法卸载, 这样便只能计算一次结果. 为此, 我们可以通过卸载应用程序域来解决.
//应用程序域用来隔离程序, 默认情况下每个进程会自动创建一个应用程序域. 实际情况下, 每个进程可以有多个应用程序域, 每个应用程序域中可以有多个线程.
//为了实现[跨应用程序域]或者[跨进程]调用, 该类需要继承MarshalByRefObject.
*/
AppDomain ad = AppDomain.CreateDomain("AnyName", null, new AppDomainSetup()); //AppDomain的静态方法创建应用程序域
object obj = ad.CreateInstanceAndUnwrap("TempClass", "TempClass"); //创建指定程序集中的类的实例并移除封装的额外信息(前面程序集名称, 后便是类名).
ITempClass itc = obj as ITempClass;
double result = itc.TempMethod();
AppDomain.Unload(ad); //使用完毕后, 卸载应用程序域
return result;
#endregion
}
private void GenerateTempClass(string expression) //临时类中含有计算用的方法
{
expression = System.Text.RegularExpressions.Regex.Replace(expression, "/", "*1.0/"); //舍去小数主要发生在除法中, 用这则表达式处理下即可
string temppath = System.Environment.GetEnvironmentVariable("Temp"); //获得用户环境变量中的地址, 通过windir获得windows目录
string itempClassNameSpace = System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace; //获得当前方法命名空间, 还可以通过GetType(CalculatorBLL).Namespace获得
using (System.IO.StreamWriter sw = new System.IO.StreamWriter(temppath + "\\TempClass.cs", false)) //使用默认编码
{
//通过字符流将类代码写入文件中, 该类需要被外界调用. MarshalByRefObject用来实现跨应用程序域or跨进程调用.
sw.WriteLine("public class TempClass : System.MarshalByRefObject , "+itempClassNameSpace+".ITempClass"); //当我们要在代码中调用TempClass中的方法时, 必须添加该类的引用, 而此时该类还不存在, 编译肯定过不了, 因此考虑用接口来做.
sw.WriteLine("{");
sw.WriteLine(" public double TempMethod()");
sw.WriteLine(" {");
sw.WriteLine(" return {0};", expression.Trim());
sw.WriteLine(" }");
sw.WriteLine("}");
}
//调用csc编译我们写的类
System.Diagnostics.ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo();
psi.FileName = System.Environment.SystemDirectory + "\\cmd.exe";
//csc编译器为位置
string cscpath = System.Environment.GetEnvironmentVariable("windir") + @"\Microsoft.NET\Framework\v3.5\csc.exe";
string refdllpath = System.Reflection.Assembly.GetExecutingAssembly().Location;
//cmd参数/c表示运行后退出; csc参数/r:表示从指定程序集文件中获得元数据, 即(从主程序的debug或release中获得程序集文件(dll或exe), 这样才能使用程序中定义的接口); 编译后的dll文件放在主窗体程序的debug或release下
psi.Arguments = " /c " + cscpath + " /t:library /r:" + refdllpath + " " + temppath + "\\TempClass.cs > " + refdllpath.Substring(0, refdllpath.LastIndexOf('\\') + 1) + "log.text";
psi.WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;
System. Diagnostics.Process p = System.Diagnostics.Process.Start(psi);
p.WaitForExit(); //主线程等待子线程p结束(类似于线程中的join()方法)
}
private string GetCurrentPath()
{
return System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); //获得不带文件名的路径
}
#endregion
}
//当我们要在代码中调用TempClass中的方法时, 必须添加该类的引用, 而此时该类还不存在, 编译肯定过不了, 因此考虑用接口来做.
public interface ITempClass
{
double TempMethod();
}
}
//Form1.cs
代码using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace Caculator
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
InitData();
}
private void InitData()
{
this.txt_exp.Text = "";
this.btn_reset.Enabled = false;
this.txt_result.Text = "";
}
private void btn_caculate_Click(object sender, EventArgs e)
{
this.btn_reset.Enabled = true;
string exp = this.txt_exp.Text.Trim();
BLL.CalculatorBLL cb = new BLL.CalculatorBLL();
this.txt_result.Text = this.txt_exp.Text + " = " +cb.Compute(exp).ToString();
}
private void btn_reset_Click(object sender, EventArgs e)
{
InitData();
}
private void btn_complexCompute_Click(object sender, EventArgs e)
{
this.btn_reset.Enabled = true;
BLL.CalculatorBLL cb = new BLL.CalculatorBLL();
this.txt_result.Text = this.txt_exp.Text + " = " + cb.Calculate(this.txt_exp.Text.Trim());
}
}
}