学习笔记---元数据、程序集、GAC版本控制、属性(Attribute)、反射(利用.NET编译器实现表达式计算器)
1. 元数据(Metadata): 元数据被存储在程序集中, 用来对程序集中的类型和方法进行描述, 程序集之所以是可描述的就是因为元数据完整的描述了程序集中每个模块的内容. 简而言之, 元数据就是用来描述数据的数据, 即元数据描述代码和数据(如类型、代码、程序集等等信息).
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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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());
}
}
}