思考并回答以下问题:
- C#的编译过程是什么样的?
- VS中的解决方案与项目是什么关系?
- 什么是类库?扩展名是.exe吗?如何创建类库?如何引用类库?
- 程序集和类库有什么区别?
- 什么是命名空间?为什么要使用命名空间?如何使用命名空间?
- 程序集包含哪四个部分?
- 什么是强命名和弱命名程序集?有什么区别?
- 如何创建强命名程序集?
- 什么是私有程序集?
本章涵盖:
- 引用其他程序集
- 命名空间
- using指令
- 程序集的结构
- 程序集标识符
- 强命名程序集
- 程序集的私有方式部署
- 共享程序集和GAC
- 配置文件
- 延迟签名
引用其他程序集
之前的学习中,我们在高层次上观察了编译过程。编译器接受源代码文件并生称名称为程序集的输出文件。这一章中,我们将详细阐述程序集以及它们是如何生成和部署的。你还会看到命名空间是如何帮助组织类型的。
在迄今为止所看到的所有程序中,大部分都声明并使用它们自己的类。然而,在许多项目中,你会想使用来自其他程序集的类或类型。这些其他的程序集可能来自BCL,或来自一个第三方供应商,或你自己创建了它们。这些程序集称为类库,而且它们的程序集文件的名称通常以.dll扩展名结尾而不是.exe扩展名。
例如,假设你想创建一个类库,它包含可以被其他程序集使用的类和类型。一个简单库的源代码如下面示例中所示,它包含在名称为SuperLib.cs的文件中。该库含有单独一个名称为SquareWidget的公有类。图1阐明了DLL的生成。
1 | // SuperLib.cs |
图1 SuperLib源代码和结果程序集
要使用Visual Studio创建类库,要在已安装的Windows模板中创建类库模板。具体来说,在Visual Studio中进行的操作步骤如下。
(1)选择File->New->Project,然后会打开New Project窗口。
(2)左边的面板,在Installed->Templates面板中找到Visual C#节点并选中。
(3)在中间的面板中选择Class Library模板。
如果选择的是Console Application(控制台应用程序),则生成的是exe。
假设你还要写一个名称为MyWidgets的程序,而且你想使用SquareWidget类。程序的代码在一个名称为MyWidgets.cs的文件中,如下面的示例所示。这段代码简单创建一个类型为SquareWidget的对象并使用该对象的成员。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// MyWidgets.cs
using System;
public class WidgetsProgram
{
static void Main()
{
SquareWidget sq = new SquareWidget(); // 来自类库
// ↑ 未在当前程序集中声明
sq.SideLength = 5.0; // 设置边长
Console.WriteLine(sq.Area); // 输出该区域
// ↑ 未在当前程序集中声明
}
}
注意,这段代码没有声明类SquareWidget。相反,使用的是定义在SuperLib中的类。然而,当你编译MyWidgets程序时,编译器必须知道你的代码在使用程序集Superlib,这样它才能得到关于类SquareWidget的信息。要实现这点,需要给编译器一个到该程序集的引用,给出它的名称和位置。
在Visual Studio中,可以用下面的方法把引用添加到项目。
- 选择Solution Explorer,并在该项目名下找到References目录。References目录包含项目使用的程序集的列表。
- 右键单击References目录并选择Add Reference。有5个可以从中选择的标签页,允许你以不同的方法找到类库
- 对于我们的程序,选择Browse标签,浏览到包含SquareWidget类定义的DLL文件,并选择它。
- 点击OK按钮,引用就被加入到项目了。
在添加了引用之后,可以编译MyWidgets了。图2阐明了全部的编译过程。
图2 引用另一个程序集
mscorlib库
有一个类库,我几乎在先前的每一个示例中都使用它。它就是包含Console类的那个库。Console类被定义在名称为mscorlib的程序集中,在名称为mscorlib.dll的文件里。然而,你不会看到这个程序集被列在References目录中。程序集mscorlib.dll含有C#类型以及大部分.NET语言的基本类型的定义。在编译C#程序时,它必须总是被引用,所以Visual Studio不把它显示在References目录中。
如果算上mscorlib,MyWidgets的编译过程看起来更像图3所示的表述。在此之后,我会假定使用mscorlib程序集而不再描述它。
图3 引用类库
现在假设你的程序已经很好地用SquareWidget类工作了,但你想扩展它的能力以使用一个名称为CircleWidget的类,它被定义在另一个名称为UltraLib的程序集中。MyWidgets的源代码看上去像下面这样。它创建一个SquareWidget对象和一个CircleWidget对象,它们分别定义在SuperLib中和UltraLib中。
1 | public class WidgetsProgram |
类库UltralLib的源代码如下面的示例所示。注意,除了类CircleWidget之外,就像库SuperLib,它还声明了一个名称为SquareWidget的类。可以把UltraLib编译成一个DLL并加入到项目MyWidgets的引用列表中。1
2
3
4
5
6
7
8
9
10
11
12
13public class SquareWidget
{
...
}
public class CircleWidget
{
public double Radius = 0;
public double Area
{
get {...}
}
}
因为两个库都含有名称为SquareWidget的类,当你试图编译程序MyWidgets时,编译器产生一条错误消息,因为它不知道使用类SquareWidgets的哪个版本。图4阐明了这种命名冲突。
图4 由于程序集SuperLib和UltraLib都含有名称为SquareWidget的类的声明,编译器不知道实例化哪一个
命名空间
在MyWidgets示例中,由于你有源代码,你能通过在SuperLib源代码或UltraLib源代码中仅仅改变SquareWidget类的名称来解决命名冲突。但是,如果这些类是由不同的公司开发的,而且你还不能拥有源代码会怎么样呢?假设SuperLib由一个名称为MyCorp的公司生产,Ultralib由ABCCorp公司生产。在这种情况下,如果你使用了任何有冲突的类或类型,你将不能把这两个库放在一起使用。
你能想象得出,在你做开发的机器上含有数打(如果不是几百个的话)不同的公司生产的程序集,很可能有一定数量的类名重复。如果仅仅因为它们碰巧有共同的类型名,不能把两个程序集用在一个程序中,这将很可惜。
但是,假设MyCorp有一个策略,让所有类的前缀都是公司名字加上类产品名和描述名。并且进一步假设ABCCorp也有相同的策略。这样的话,我们示例中的3个类名就可能是MyCorpSuperLibSquareWidget、ABCCorpUltraLibSquareWidget和ABCCorpUltraLibCircleWidget,如图5所示。这当然是完全有效的类名,并且一个公司类库的类不太可能与其他公司类库的类发生冲突。
图5 有了消除歧义的字符串前缀,类库之间不会有冲突
但是,在我们的示例程序中,需要使用冗长的名字,看上去如下所示:1
2
3
4
5
6
7
8class WidgetsProgram
{
static void Main()
{
MyCorpSuperLibSquareWidget sq = new MyCorpSuperLibSquareWidget(); // 来自SuperLib
ABCCorpUltraLibCircleWidget circle = new ABCCorpUltraLibCircleWidget(); // 来自UltraLib
}
}
尽管这可以解决冲突问题,但是即使有智能感知,这些新的、已消除歧义的名字还是难以阅读并且容易出错。
不过,假设除了标识符中一般允许的字符,还可以在字符串中使用点——尽管不是在类名的最前面或最后面,那么这些名字就更好理解了,比如MyCorp.SuperLib.SquareWidget、ABCCorp.UltraLib.SquareWidget及ABCCorp.UltraLib.CircleWidget。现在代码看上去如下所示:1
2
3
4
5
6
7
8class WidgetsProgram
{
static void Main()
{
MyCorp.SuperLib.SquareWidget sq = new MyCorp.SuperLib.SquareWidget(); // 来自SuperLib
ABCCorp.UltraLib.CircleWidget circle = new ABCCorp.UltraLib.CircleWidget(); // 来自UltraLib
}
}
这就给了我们命名空间名和命名空间的定义。
- 你可以把命名空间名视为一个字符串(在字符串中可以使用点),它加在类名或类型名的前面并且通过点进行分隔。
- 包括命名空间名、分隔点,以及类名的完整字符串叫做类的完全限定名。
- 命名空间是共享命名空间名的一组类和类型。
图6演示了这些定义。
图6 命名空间是共享同一命名空间名的一组类型定义
你可以使用命名空间来把一组类型组织在一起并且给它们起一个名字。一般而言,命名空间名描述的是命名空间中包含的类型,并且和其他命名空间名不同。
你可以通过在包含你的类型声明的源文件中声明命名空间,从而创建命名空间。如下代码演示了声明命名空间的语法。然后在命名空间声明的大括号中声明你的所有类和其他类型。那么这些类型就是这个命名空间的成员了。1
2
3
4
5// ↓ 关键字 ↓ 命名空间名
namespace NamespaceName
{
TypeDeclarations
}
如下代码演示了MyCorp的程序员如何创建MyCorp.SuperLib命名空间以及声明其中的SquareWidget类。1
2
3
4
5
6
7
8
9
10
11
12
13// ↓ 公司名
namespace MyCorp.SuperLib
{
// ↑ 点
public class SquareWidget
{
public double SideLength = 0;
public double Area
{
get{ return SideLength * SideLength; }
}
}
}
当MyCorp公司给你配上更新的程序集时,你可以通过按照如下方式修改MyWidgets程序来使用它。1
2
3
4
5
6
7
8
9
10
11
12class WidgetsProgram
{
static void Main()
{
// MyCorp.SuperLib.SquareWidget是完全限定名
// MyCorp.SuperLib是命名空间名
// SquareWidget是类名
MyCorp.SuperLib.SquareWidget sq = new MyCorp.SuperLib.SquareWidget();
CircleWidget circle = new CircleWidget();
...
}
}
既然你在代码中显式指定了SquareWidget的SuperLib版本,编译器不会再有区分类的问题了。完全限定名称输入起来有点长,但至少你现在能使用两个库了。在本章稍后,会阐述using别名指令以解决不得不在完全限定名称中重复输入的麻烦。
如果UltraLib程序集也被生产它的公司(ABCCorp)使用命名空间更新,那么编译过程会如图7所示。
图7 带命名空间的类库
命名空间名称
如你所见,命名空间的名称可以包含创建该程序集的公司的名称。除了标识公司以外,该名称还用于帮助程序员快速了解定义在命名空间内的类型的种类。关于命名空间名称的一些要点如下。
- 命名空间名称可以是任何有效标识符。
- 另外,命名空间名称可以包括句点符号,用于把类型组织成层次。
例如,表1列出了一些在.NET BCL中的命名空间的名称。
表1 来自BCL的命名空间示例
System | System.IO |
---|---|
System.Data | Microsoft.CSharp |
System.Drawing | Microsoft.VisualBasic |
下面是命名空间命名指南:
- 使用公司名开始命名空间名称;
- 在公司名之后跟着技术名称;
- 不要把命名空间命名为与类或类型相同的名称。
例如,Acme Widget公司的软件开发部门在下面3个命名空间中开发软件,如下面的代码所示:
- AcmeWidgets.SuperWidget
- AcmeWidgets.Media
- AcmeWidgets.Games
1 | namespace AcmeWidgets.SuperWidget |
命名空间的补充
关于命名空间,有其他几个要点应该知道。
- 在命名空间内,每个类型名必须有别于所有其他类型。
- 命名空间内的类型称为命名空间的成员。
- 一个源文件可以包含任意数目的命名空间声明,可以顺序也可以嵌套。
图8在左边展示了一个源文件,它顺序声明了两个命名空间,每个命名空间内有几个类型。注意,尽管命名空间内含有几个共有的类名,它们被命名空间名称区分开来,如右边的程序集所示。
图8 多个命名空间在一个源文件中
.NET框架BCL提供了数千个已定义的类和类型以供生成程序时选择。为了帮助组织这组有用的功能,相关功能的类型被声明在相同的命名空间里。BCL使用超过100个命名空间来组织它的类型。
命名空间跨文件伸展
命名空间不是封闭的。这意味着可以在该源文件的后面或另一个源文件中再次声明它,以对它增加更多的类型声明。
例如,图9展示了三个类的声明,它们都在相同的命名空间中,但声明在分离的源文件中。源文件可以被编译成单一的程序集,如图9所示,或编译成外离的程序集,如图10所示。
图9 命名空间可以跨源文件伸展并编译成单一程序集
图10 命名空间可以跨源文件伸展并编译成分离的程序集
嵌套命名空间
命名空间可以被嵌套,从而产生嵌套的命名空间。嵌套命名空间允许你创建类型的概念层次。有两种方法声明一个嵌套的命名空间,如下所示。
- 原文嵌套
可以把命名空间的声明放在一个封装的命名空间声明体内部,从而创建一个嵌套的命名空间。图11的左边阐明了这种方法。在这个示例中,命名空间OtherNs嵌套在命名空间MyNamespace中。 - 分离的声明
也可以为嵌套命名空间创建分离的声明,但必须在声明中使用它的完全限定名称。图11的右边阐明了这种方法。注意在嵌套命名空间OtherNs的声明中,使用全路径命名MyNamespace.OtherNs.
图11 声明嵌套命名空间的两种形式是等价的
图11所示的两种形式的嵌套命名空间声明生成相同的程序集,如图12所阐明的。该图展示了两个声明在SomeLib.cs文件中的类,使用它们的完全限定名。
图12 嵌套命名空间结构
虽然嵌套命名空间位于父命名空间内部,但是其成员并不属于包裹的父命名空间。有一个常见的误区,认为既然嵌套的命名空间位于父命名空间内部,其成员也是父命名空间的子集,这是不正确的,命名空间之间是相互独立的。
using指令
完全限定名可能相当长,在代码中通篇使用它们会变得十分乏味。然而,有两个编译器指令,可以使你避免不得不使用完全限定名:using命名空间指令和using别名指令。
关于using指令的两个要点如下。
- 它们必须放在源文件的顶端,在任何类型声明之前。
- 它们应用于当前源文件中的所有命名空间。
using命名空间指令
在MyWidgets示例中,你看到多个部分使用完全限定名称指定一个类。可以通过在源文件的顶端放置using命名空间指令以避免不得不使用长名称。
using命名空间指令通知编译器你将要使用来自某个指定命名空间的类型。然后你可以继续,并使用简单类名而不必全路径修饰它们。
当编译器遇到一个不在当前命名空间的名称时,它检查在using命名空间指令中给出的命名空间列表,并把该未知名称加到列表中的第一个命名空间后面。如果结果完全限定名称匹配了这个程序集或引用程序集中的一个类,编译器将使用那个类。如果不匹配,那么它试验列表中下一个命名空间。
using命名空间指令由关键字using跟着一个命名空间标识符组成。1
2
3// ↓ 关键字
using System;
// ↑ 命名空间的名称
一个已经在通篇文字中使用的方法是WriteLine方法,它是类Console的成员,在System命名空间中。不是在通篇代码中使用它的完全限定名,我只是简化了一点我们的工作,在代码的顶端使用using命名空间指令。
例如,下面的代码在第一行使用using命名空间指令以描述该代码使用来自System命名空间的类或其他类型。1
2
3
4using System; // using命名空间指令
...
System.Console.WriteLine("This is text 1"); // 使用完全限定名称
Console.WriteLine("This is text 2"); // 使用指令
using别名指令
using别名指令允许起一个别名给:
- 命名空间;
- 命名空间内的一个类型。
例如,下面的代码展示了两个using别名指令的使用。第一个指令告诉编译器标识符Syst是命名空间System的别名。第二个指令表明标识符SC是类System.Console的别名。1
2
3
4// ↓ 关键字 ↓ 命名空间
using Syst = System;
using SC = System.Console;
// ↑ 别名 ↑ 类
下面的代码使用这些别名。在Main中3行代码都调用System.Console.WriteLine方法。
- Main的第一条语句使用命名空间(System)的别名。
- 第二条语句使用该方法的完全限定名。
- 第三条语句使用类(Console)的别名。
1 | using Syst = System; // using别名指令 |
程序集的结构
程序集不包含本地机器代码,而是公共中间语言代码。它还包含实时编译器(JIT)在运行时转换CIL到本机代码所需的一切,包括对它所引用的其他程序集的引用。程序集的文件扩展名通常为.exe或.dll.
大部分程序集由一个单独的文件构成。图13阐明了程序集的4个主要部分。
- 程序集的清单(manifest)包含以下几点。
- 程序集名称标识符。
- 组成程序集的文件列表。
- 一个指示程序集中内容在哪里的地图。
- 关于引用的其他程序集的信息。
- 类型元数据部分包含该程序集中定义的所有类型的信息。这些信息包含关于每个类型要知道的所有事情。
- CIL部分包含程序集的所有中间代码。
- 资源部分是可选的,但可以包含图形或语言资源。
图13 单文件程序集的结构
程序集代码文件称为模块。尽管大部分程序集由单文件组成,但有些也有多个文件。对于有多个模块的程序集,一个文件是主模块(primary module),而其他的是次要模块(secondary modules)。
- 主模块含有程序集的清单和到次要模块的引用。
- 次要模块的文件名以扩展名.netmodule结尾。
- 多文件程序集被视为一个单一单元。它们一起部署并一起定版。
图14闸明了一个带次要模块的多文件程序集。
图14 多文件程序集
程序集标识符
在.NET框架中,程序集的文件名不像在其他操作系统和环境中那么重要,更重要的是程序集的标识符(identity)。
程序集的标识符有4个组成部分,它们一起唯一标识了该程序集,如下所示。
- 简单名 这只是不带文件扩展名的文件名。每个程序集都有一个简单名。它也被称为程序集名或友好名称( friendly name )。
- 版本号 它由4个句点分开的整数字符串组成,形式为MajorVersion.MinorVersion.Build.Revision,例如2.0.35.9.
- 文化信息 它是一个字符串,由2-5个字符组成,代表一种语言,或代表一种语言和一个国家或地区。例如,在美国使用英语的文化名是en-US。在德国使用德语,它是de-DE。
- 公钥 这个128字节字符串应该是生产该程序集的公司唯一的。
公钥是公钥/私钥对的一部分,它们是一组两个非常大的、特别选择的数字,可以用于创建安全的数字签名。公钥,顾名思义,可以被公开。私钥必须被拥有者保护起来。公钥是程序集的标识符的一部分。我们稍后会在本章看到私钥的使用。
程序集名称的组成被包含在程序集清单中。图15阐明了清单部分。
图15 清单中程序集标识符的组成部分
图16展示了用在.NET文档和书籍中的关于程序集标识符的一些术语。
图16 关于程序集标识符的术语
强命名程序集
强命名(strongly named)程序集有一个唯一的数字签名依附于它。强命名程序集比没有强名称的程序集更加安全,原因有以下几点。
- 强名称唯一标识了程序集。没有其他人能创建一个与之有相同名称的程序集,所以用户可以确信该程序集来自于其声称的来源。
- 没有CLR安全组件来捕获更改,带强名称的程序集的内容不能被改变。
弱命名(weakly named)程序集是没有被强命名的程序集。由于弱命名程序集没有数字签名,它天生是不安全的。因为一根链的强度只和它最弱的一环相同,所以强命名程序集默认只能访问其他强命名程序集(还存在一种方法允许“部分地相信调用者”,但我不会阐述这个主题)。
程序员不产生强名称。编译器产生它,接受关于程序集的信息,并散列化(hash)这些信息以创建一个唯一的数据签名依附到该程序集。它在散列处理中使用的信息如下:
- 组成程序集的字节序列;
- 简单名称;
- 版本号;
- 文化信息;
- 公钥/私钥对。
创建强命名程序集
要使用Visual Studio强命名一个程序集,必须有一份公钥/私钥对文件的副本。如果没有密钥文件,可以让Visual Studio产生一个。可以实行以下步骤。
(1)打开工程的属性。
(2)选择签名页。
(3)选择Sign the Assembly复选框并输入密钥文件的位置或创建一个新的。
在编译代码时,编译器会生成一个强命名的程序集。编译器的输入和输出在图17中阐明。
图17 创建强命名程序集
说明
要创建强命名程序集还可以使用Strong Name工具(sn.exe),这个工具在安装Visual Studio的时候会自动安装。它是个命令行工具,允许程序员为程序集签名,还能提供大量管理密钥和签名的其他选项。如果Visual Studio IDE还不符合你的要求,它能提供更多选择。
程序集的私有方式部署
在目标机器上部署一个程序就像在该机器上创建一个目录并把应用程序复制过去一样简单。如果应用程序不需要其他程序集(比如DLL),或如果所需的DLL在同一目录下,那么程序应该会就在它所在的地方良好工作。这种方法部署的程序集称为私有程序集,而且这种部署方法称为复制文件(XCopy)部署。
私有程序集几乎可以被放在任何目录中,而且只要它们依赖的文件都在同一目录或子目录下就足够了。事实上,可以在文件系统的不同部分有多个目录,每个目录都有同样的一组程序集,并且它们都会在它们各自不同的位置良好工作。
关于私有程序集部署的一些重要事情如下。
- 私有程序集所在的目录被称为应用程序目录。
- 私有程序集可以是强命名的也可以是弱命名的。
- 没有必要在注册表中注册组件。
- 要卸载一个私有程序集,只要从文件系统中删除它即可。
共享程序集和GAC
私有程序集是非常有用的,但有时你会想把一个DLL放在一个中心位置,这样一个单独的复制就能被系统中其他的程序集共享。.NET有这样的贮藏库,称为全局程序集缓存(GAC)。放进GAC的程序集称为共享程序集。
关于GAC的一些重要内容如下。
- 只有强命名程序集能被添加到GAC.
- GAC的早期版本只接受带.dll扩展名的文件,现在也可以添加带.exe扩展名的程序集了。
- GAC位于Windows系统目录的子目录中。.NET4.0之前位于\Windows\Assembly中,从.NET4.0开始位于\Windows\Microsoft.NET\assembly中。
把程序集安装到GAC
当试图安装一个程序集到GAC时,CLR的安全组件首先必须检验程序集上的数字签名是否有效。如果没有数据签名,或它是无效的,系统将不会把它安装到GAC.
然而,这是个一次性检查。在程序集已经在GAC内之后,当它被一个正在运行的程序引用时,不再需要进一步的检查。
gacutil.exe命令行工具允许从GAC添加或删除程序集,并列出GAC包含的程序集。它的3个最有用的参数标记如下所示
- /i:把一个程序集插入GAC。
- /u:从GAC卸载一个程序集。
- /l:列出GAC中的程序集。
GAC内的并肩执行
在程序集部署到GAC之后,它就能被系统中其他程序集使用了。然而,请记住程序集的标识符由完全限定名称的全部4个部分组成。所以,如果一个库的版本号改变了,或如果它有一个不同的公钥,这些区别指定了不同的程序集。
结果就是在GAC中可以有许多不同的程序集,它们有相同的文件名。虽然它们有相同的文件名,但它们是不同的程序集而且在GAC中完美地共存。这使不同的应用程序在同一时间很容易使用不同版本的同一DLL,因为它们是带不同标识符的不同程序集。这被称为并肩执行(side-by-side Execution)。
图18阐明了GAC中4个不同的DLL,它们都有相同的文件名MyLibary.dll。看这个图,可以看出前3个来自于同一公司,因为它们有相同的公钥,第4个来源不同,因为它有一个不同的公钥。这些版本如下:
- 英文V1.0.0.0版,来自A公司;
- 英文V2.0.0.0版,来自A公司;
- 德文V1.0.0.0版,来自A公司;
- 英文V1.0.0.0版,来自B公司。
图18 在GAC中4个不同的并肩DLL
配置文件
配置文件含有关于应用程序的信息,供CLR在运行时使用。它们可以指示CLR去做这样的事情,比如使用一个不同版本的DLL,或搜索程序引用的DLL时在附加目录中查找。
配置文件由XML代码组成,并不包含C#代码。编写XML代码的细节超出了本书的范围,但应当理解配置文件的目的以及它们如何使用。它们的一种用途是更新一个应用程序集以使用新版本的DLL。
例如,假设有一个应用程序引用了GAC中的一个DLL。在应用程序的清单中,该引用的标识符必须完全匹配GAC中程序集的标识符。如果一个新版本的DLL发布了,它可以被添加到GAC中,在那里它可以幸福地和老版本共存。
然而,应用程序仍然在它的清单中包括老版本DLL的标识符。除非重新编译应用程序并使它引用新版本的DLL,否则它会继续使用老版本。如果这是你想要的,那也不错。
然而,如果你不想重新编译程序但又希望它使用新的DLL,那么你可以创建一个配置文件告诉CLR去使用新的版本而不是旧版本。配置文件被放在应用程序目录中。
图19阐明了运行时过程中的对象。左边的应用程序MyProgram.exe调用MyLibrary.dll的1.0.0.0版,如点化线箭头所示。但应用程序有一个配置文件,而它指示CLR加载2.0.0.0版。注意配置文件的名称由可执行文件的全名(包括扩展名)加上附加扩展名.config组成。
图19 使用配置文件绑定一个新版本
Unity
Unity的Project的名字就是解决方案的名字。
Unity创建Project后并新建一个脚本后,VS中项目默认的名字是Assembly-CSharp。
引用下面的都是类库。例如System.dll。