首页 > 代码库 > 改善C#编程的50个建议(46-50)
改善C#编程的50个建议(46-50)
-------------------------翻译 By Cryking-----------------------------
-----------------------转载请注明出处,谢谢!------------------------
异常是一种报告错误的机制,它可以在远离错误发生的地方处理错误.所有关于错误发生的的信息必须包含在异常对象中.在错误发生的过程中,你可能想把底层的错误转化
成详细的应用程序错误,而且不丢失关于原始错误的任何信息.当你的应用创建你自己的特定异常类时,你需要考虑这些问题.
第一步就是理解什么时候以及为什么创建新的异常类,并且如何去构造异常的信息层次.当开发者使用你的库来写catch语句时,他们基于特定的运行时异常来进行区分.每个
不同的异常类可能有不同的处理要做。
try
{
Foo();
Bar();
}
catch (MyFirstApplicationException e1)
{
FixProblem(e1);
}
catch (AnotherApplicationException e2)
{
ReportErrorAndContinue(e2);
}
catch (YetAnotherApplicationException e3)
{
ReportErrorAndShutdown(e3);
}
catch (Exception e)
{
ReportGenericError(e);
throw;
}
finally
{
CleanupResources();
}
不同的catch语句可能为不同的运行时异常而存在.你,作为应用开发者,当catch语句可能要处理不同的事情时,必须创建或使用不同的异常类.注意上面每个不同的异常都
以不同的方式进行处理。当异常处理不同时,开发者仅仅想为不同的异常类提供不同的catch语句来处理.否则,那就是额外的工作.因此,当你相信开发者将对不同异常产生
的问题进行不同的处理时,你应考虑创建不同的异常类.如果不这样做,当一个异常抛出时,你可能会终止应用.这当然会省掉一些代码,但会使你失去用户。或者,它们可以
通过异常来尝试觉得是否错误是正确发生的:
private static void SampleTwo()
{
try
{
Foo();
Bar();
}
catch (Exception e)
{
switch (e.TargetSite.Name)
{
case "Foo":
FixProblem(e);
break;
case "Bar":
ReportErrorAndContinue(e);
break;
// some routine called by Foo or Bar:
default:
ReportErrorAndShutdown(e);
throw;
}
}
finally
{
CleanupResources();
}
}
这远没有使用多个catch语句有吸引力。它是很脆弱的代码:如果你改变了常规的名称,它就被破坏了.如果你移动错误生成的调用函数到一个共享工具函数中,
它也被破坏了.异常发生的堆栈越深,就会使这样的结构变得越脆弱.
在开始深入了解本章之前,让我先声明两点。第一,异常并不适合处理你遇到的每个错误。没有固定的路线,但我喜欢为错误条件抛出异常,这些错误条件
如果不立即处理或者被报告,可能会导致一个长期的问题。例如,数据库的数据完整性错误就应产生一个异常。这个问题如果忽略将会变得越来越严重。而写入
用户窗口位置失败时不太可能导致广泛的问题。返回一个错误代码来指示失败就足了。第二,写一个throw语句并不意味着需要马上创建一个新的异常类。我推荐
创建更多的异常类,而不是只有少数的几个常规异常类:人们在抛出异常的任何时候都喜欢过渡地使用System.Exception.那个异常类只提供少数有用的信息来
处理调用代码。相反,考虑创建一些必要的异常类,可以让调用代码明白错误原因,而且提供了最好的机会来恢复它。再说一次,创建不同异常类的原因--事实上,
唯一的原因是让你的用户在写catch语句来处理错误时更简单。查看分析这些错误条件,看哪些可以候选成为一个可以恢复错误的行为,然后创建指定的异常类来处理这些行为.你的应用程序可以从一个文件或者目录丢失的错误中恢复过来吗?还可以从权限不足的错误下恢复吗?丢失网络资源的时候呢?对于这种遇到不同的错误,可能要采取不同的恢复机制时,你应该为不同的行为创建新的异常类。
所以现在你应该创建你自己的异常类了。当你创建一个新的异常类时,你承担了一些特定的责任。你的异常类必须以"Exception"结束。你应总是从System.Exception类或一些其他合适的异常类来派生你自己的异常类。对于基类,你将很少添加功能。建不同异常类的目的是在catch语句中有能力去区分导致的错误。
但也不要从你创建的异常类中删除任何东西。异常类包含四个构造函数:
// Default constructor
public Exception();
// Create with a message.
public Exception(string);
// Create with a message and an inner exception.
public Exception(string, Exception);
// Create from an input stream.
protected Exception(SerializationInfo, StreamingContext);
当你创建一个新的异常类,你也应创建这四个构造函数。注意最后一个构造函数意味着你的异常类必须是可序列化的。不同的情况调用不同的构造函数。你可以委托这个工作给基类来实现:
[Serializable]
public class MyAssemblyException :
Exception
{
public MyAssemblyException() :base()
{
}
public MyAssemblyException(string s) :base(s)
{
}
public MyAssemblyException(string s,
Exception e) :base(s, e)
{
}
protected MyAssemblyException(SerializationInfo info, StreamingContext cxt) :
base(info, cxt)
{
}
}
带一个异常参数的构造函数值得讨论一下。有时,你使用的一个库产生了一个异常。当你从你使用的工具简单地传递异常时,调用你的库的代码可能会取得最小的关于可能修正行为的信息:
public double DoSomeWork()
{
// This might throw an exception defined
// in the third party library:
return ThirdPartyLibrary.ImportantRoutine();
}
当你产生这个异常的时候,你应提供你自己的库的信息。抛出你自己的特定异常并将原始异常作为InnerException的属性。你应尽可能地提供最多的额外信息:
public double DoSomeWork()
{
try
{
// This might throw an exception defined
// in the third party library:
return ThirdPartyLibrary.ImportantRoutine();
}
catch (ThirdPartyException e)
{
string msg =
string.Format("Problem with {0} using library",
ToString());
throw new DoingSomeWorkException(msg, e);
}
}
这个新版本的方法将在问题发生点创建更多的信息。一旦你创建了一个合适的ToString()方法,你就已经创建了一个可以完整描述问题发生的异常对象.更多的是,内部异常显示了产生问题的根源:可能来自你使用的第三方库。
这个技术被称为异常转换。转换一个底层异常到一个更高级的异常,这可以提供更多关于错误上下文的信息。当一个错误发生时,你生成的信息越多就越容易被用户用来诊断并修正错误。通过创建你自己的异常类型,你可以转换底层的问题到包含所有特定应用程序信息的特定的异常,这可以帮助你全面诊断程序以及尽可能的修正错误。
希望你的应用程序不是经常抛出异常,但是它将会抛出。如果你不做任何特定的处理,不管你调用的方法里发生什么错误,你的应用将生成缺省的.NET框架异常。提供更详细的信息将会让你以及你的用户,在实际应用中诊断程序以及可能的修正错误大有帮助。当且仅当对于错误有不同的行为要处理时,你才应该创建不同的异常类。通过提供基类异常支持的所有构造函数,你创建全功能的异常类。你使用InnerException属性来运载底层错误条件的所有错误信息。
47 选择强异常的保护
当你抛出一个异常,你就在应用程序中引入了一个分裂事件。控制流被打乱。预期的动作将不会发生。更坏的是你还要把清理工作留给最终写代码捕获了异常的程序员。当一个异常发生时,如果你可以从你所管理的程序状态中直接捕获,那么你还可以采取一些有效的方法.感谢的是C#团体不需要创建它自己的异常安全策略。C++团体已经为我们做了所有困难的工作。我们应在C#中利用所有那些已完成的困难工作。
Dave Abrahams定义了三种安全异常来保证程序:基本保护,强保护,以及无抛出保护.Herb Sutter在他的书Exceptional C++中讨论了这些保护。基本保护状态是指没有资源泄漏,而且所有的对象在你的应用程序抛出异常后是可用的。强保护建立在基本保护上,并增加了如果一个异常发生,程序状态不发生改变。无抛出保护状态是指操作从不会失败,从不会抛出异常。强异常保护是异常中恢复和简单异常之间最平衡的一个。
你可以从.NET CLR上获得一些基本保护。CLR环境处理了内存管理。只有在一种情况下,你可能会有资源泄漏,那就是在异常抛出时,你的程序占有一个实现了IDisposable接口的资源对象。第18条解释了如何在异常时避免资源泄漏。但这仅仅是保护的一部分。你仍需要负责确保你的对象状态是可用的。假设你的类型不断地收集,缓存了一个个集合。当Add()操作抛出异常时,你需要确保这个大小匹配了实际的存储。如果应用处在部分完成的无效状态,这里有无数的动作将会发生。它们很难处理,因为缺少自动支持的标准方法。通过坚持强保护,很多这样的问题可以被完美的解决。
强保护状态是指如果一个操作因为异常终止,程序状态仍不改变。不管操作是否完成,都不修改程序的状态,没有中间可能。强保护的好处是当抓主一个异常后,你可以更容易地继续执行程序。任何时候你抓住了一个异常,不管操作尝试是否已经发生。它不会开始,也不会做任何修改。程序的状态就像你没有开始那个处理一样。
之前我做的很多建议都将帮你确保你满足强异常保护。程序使用的数据元素应存储为不可变的值类型。你也能使用函数式编程风格,如LINQ查询。那种编程风格自动遵循了强异常保护。
有时,你不能使用函数式编程风格。如果你联合这两点,任何对程序状态进行的修改都可以在任何可能引发异常的操作完成后简单的发生。常规的原则是让任何数据的修改都遵守下面的原则:
1.对可能被修改的数据进行被动复制。
2.在复制的数据上执行任何修改。这包括任何可能抛出异常的操作。
3.交换临时的副本数据到源数据。这个操作不能抛出异常。
下面的例子,使用被动复制更新一个职工的标题和工资:
public void PhysicalMove(string title, decimal newPay)
{
// 工资单数据是一个结构体:
//如果字段无效,构造函数将抛出一个异常
PayrollData d = new PayrollData(title,newPay,this.payrollData.DateOfHire);
// 如果d是已经构造的属性, 交换它:
this.payrollData = http://www.mamicode.com/d;
}
有时,强保护因效率太低而不被支持,而且有些时候,你不能支持没有潜在BUG的强保护。第一个最简单的例子是循环结构。当代码在一个循环里修改了程序的状态,并且可能抛出一个异常,你会面对一个艰难的选择:你要么对循环里使用的所有对象进行被动拷贝,要么降低异常保护,只去支持基本异常保护。这里没有困难和快速的规则,但在托管环境里拷贝堆分配的对象,并不像在本地环境上那开销昂贵。在.NET里,大量的时间都花在了优化内存管理上。我喜欢选择支持强异常保护,即使这意味要复制一个大的容器:这种从错误中恢复的能力要比避免拷贝而获得小的性能更有价值。 更大的担心是交换引用类型可能导致程序错误。考虑下面这个例子:
private BindingList<PayrollData> data;
public IBindingList MyCollection
{
get { return data; }
}
public void UpdateData()
{
// Unreliable operation might fail:
var temp = UnreliableOperation();
// This operation will only happen if
// UnreliableOperation does not throw an exception.
data = http://www.mamicode.com/temp;
}
这看起来很好地使用了被动复制机制。你创建了数据的副本。然后你从某处抓取新数据来填充临时数据。最后,你把临时存储交换回来。这看起来很不错。如果在检索数据中发生了任何错误,你就相当于没有做任何修改。
这只有一个问题:它不能工作.MyCollection属性返回了数据对象的一个引用。所有这个类的客户,在你调用了UpdateData后,还是保持着原BindingList<>的引用。他们所看到的是旧数据的视图。交换的伎俩在引用类型上不工作,它只能在值类型上工作。为了修正这个问题,你需要替换当前引用对象的数据,并且确保你这样做不会抛出异常。这是很困难的,因为它是两个不同的原子操作:从集合里移除所有已存在的对象并增加所有的新对象。你可能考虑到风险是移除一些项并添加新项:
private BindingList<PayrollData> data;
public IBindingList MyCollection
{
get
{
return data;
}
}
public void UpdateData()
{
// Unreliable operation might fail:
var temp = UnreliableOperation();
// These operations will only happen if UnreliableOperation does not throw an exception.
data.Clear();
foreach (var item in temp)
data.Add(item);
}
这是合理的,但也不是完美的解决方案。我提到它是因为通常合理的就是你需要的。但是当你需要做到完美时,你需要做更多的工作。封信模式(envelope-letter pattern)将隐藏内部交换在一个对象里,这样可以使交换更安全。封信模式隐藏具体实现到一个封装里,它可以使你与公共客户分享你的代码。在这个例子里,你将创建一个类来封装集合和实现IBindingList<PayrollData>.那个类包含BindingList<PayrollData>并暴露所有他的方法。
你的类现在和信封类一起来处理它的内部数据.
private Envelope data;
public IBindingList MyCollection
{
get
{
return data;
}
}
public void UpdateData()
{
data.SafeUpdate(UnreliableOperation());
}
信封类通过转发每个请求到BindingList<PayrollData>来实现IBindingList.
public class Envelope : IBindingList
{
private BindingList<PayrollData> data =http://www.mamicode.com/new BindingList
#region IBindingList Members
public void AddIndex(PropertyDescriptor property)
{ (data as IBindingList).AddIndex(property); }
public object AddNew() { return data.AddNew(); }
public bool AllowEdit { get { return data.AllowEdit; } }
public bool AllowNew { get { return data.AllowNew; } }
public bool AllowRemove{ get { return data.AllowRemove; } }
public void ApplySort(PropertyDescriptor property,
ListSortDirection direction)
{
(data as IBindingList).ApplySort(property, direction);
}
public int Find(PropertyDescriptor property, object key)
{ return (data as IBindingList).Find(property, key); }
public bool IsSorted
{ get { return (data as IBindingList).IsSorted; } }
private ListChangedEventHandler listChangedHandler;
public event ListChangedEventHandler ListChanged
{
add { listChangedHandler += value; }
remove { listChangedHandler -= value; }
}
public void RemoveIndex(PropertyDescriptor property)
{ (data as IBindingList).RemoveIndex(property); }
public void RemoveSort()
{ (data as IBindingList).RemoveSort(); }
public ListSortDirection SortDirection
{ get { return (data as IBindingList).SortDirection; } }
public PropertyDescriptor SortProperty
{ get { return (data as IBindingList).SortProperty; } }
public bool SupportsChangeNotification
{
get
{
return (data as IBindingList).SupportsChangeNotification;
}
}
public bool SupportsSearching
{ get { return (data as IBindingList).SupportsSearching; } }
public bool SupportsSorting
{ get { return (data as IBindingList).SupportsSorting; } }
#endregion
#region IList Members
public int Add(object value)
{
if (value is PayrollData)
data.Add((PayrollData)value);
return data.Count;
}
public void Clear() { data.Clear(); }
public bool Contains(object value)
{
if (value is PayrollData)
return data.Contains((PayrollData)value);
else
// 如果参数不是正确的类型,他不能在这里
return false;
}
public int IndexOf(object value)
{
if (value is PayrollData)
return data.IndexOf((PayrollData)value);
else
return -1;
}
public void Insert(int index, object value)
{
if (value is PayrollData)
data.Insert(index, (PayrollData)value);
}
public bool IsFixedSize
{ get { return (data as IBindingList).IsFixedSize; } }
public bool IsReadOnly
{ get { return (data as IBindingList).IsReadOnly; } }
public void Remove(object value)
{
if (value is PayrollData)
data.Remove((PayrollData)value);
}
public void RemoveAt(int index)
{ data.RemoveAt(index); }
public object this[int index]
{
get { return data[index]; }
set
{
if (value is PayrollData)
data[index] = (PayrollData)value;
}
}
#endregion
#region ICollection Members
public void CopyTo(Array array, int index)
{
(data as System.Collections.ICollection).CopyTo(array, index);
}
public int Count { get { return data.Count; } }
public bool IsSynchronized
{
get
{
return (data as
System.Collections.ICollection).IsSynchronized;
}
}
public object SyncRoot
{
get
{
return (data as System.Collections.ICollection).SyncRoot;
}
}
#endregion
#region IEnumerable Members
public System.Collections.IEnumerator GetEnumerator()
{ return data.GetEnumerator(); }
#endregion
public void SafeUpdate(IEnumerable<PayrollData>
bindingList)
{
// make the copy:
BindingList<PayrollData> updates =
new BindingList<PayrollData>(bindingList.ToList());
// swap:
System.Threading.Interlocked.Exchange
<BindingList<PayrollData>>(ref data, updates);
}
}
这里有很多样板代码要去检查,并且大部分都是比较明确的。但是,这里有几个你应留意检查的重要部分。首先,注意IBindingList接口的一些成员通过BindingList<T>类显示实现了。这是cast包含这么多方法的原因。同时,我也把PayrollData当做值类型来编码。如果PayrollData是一个引用类型,这个代码将会简单一点。我把PayrollData当值类型是为了演示它们的不同。类型检查就是把PayrollData当作一个值类型来处理的。最后,注意ListChangedEventHandler必须被显示实现,这样你才能转发事件句柄到包含信封的对象。
当然,这个练习点是创建和实现了SafeUpdate方法。注意它本质上做的工作和之前一样。唯一不同的是交换通过调用Interlocked.Exchange来完成。这样保证了代码是安全的,甚至在多线程应用中也是安全的。这个交换不能被中断。
在通常情况下,你不能修正引用类型交换的问题,并同时确保所有的客户有该对象正确的副本。交换工作只是针对值类型的。如果你遵循第18条建议的化,那样才足够。
最后,最严格的,就是无抛出保护。无抛出保护几乎就像它的字面一样:如果它能保证从始至终都决不会有异常从方法中出现,这个方法就满足无抛出保护。这对所有日常的大型程序而言是不实际的。但在几个地方,方法必须强制无抛出包含。Finalizers和Dispose方法必须不抛出异常。在这两个方法里,抛出一个异常可能导致更多问题。在finalizer里,抛出一个异常将终止程序,从而使得应用得不到进一步清理。在try/catch里封装一个大方法并拦截所有异常,你是这样实现无抛出保护。满足无抛出保护的大多数方法,如Dispose()和Finalize(),有受限的责任。因此,你应能编写这些方法,这样他们能通过编写防御代码来满足无抛出异常。
在一个Dispose方法抛出一个异常时,系统可能有两个异常在运行。.NET环境失去第一个异常并抛出新的异常。在你程序的任何地方,你不能抓住最初的异常;它被系统拦截了。这大大增加了你错误处理的复杂度。你如何从一个看不见的错误里恢复?
无抛出异常的最后一点是在委托目标里。当一个委托目标抛出一个异常,在这个多播委托上的其它目标就不能被调用了.对于这个问题的唯一方法就是确保你在委托目标上不抛出任何异常。我们再重申一次:委托目标(包括事件处理程序)不应抛出异常。这样做就意味着引发事件的代码不应该参与到强异常保护中。但在这里,我将要修改这一建议.第24条展示了如何调用委托以致于你能从异常中恢复过来。不过,不是每个人都这样做,所以你应避免在委托句柄里抛出异常。只是因为你在委托上不抛出异常,并不意味着其它人也遵守这一建议,在你自己的委托调用上不能信任它是无抛出的保护。这就是防御式编程:你应尽可能做到最好,因为其他程序员可能尽可能地做到最坏了。
异常的引入严重地改变了应用程序的控制流。最坏的情况下,任何事情都可能发生,或不发生。在异常发生时,唯一可以知道哪些事情发生,哪些事情没有发生的方法就是强制强异常保护。当一个操作不管是完成还是没有完成时都不做任何修改。Finalizers,Dispose(),以及委托目标都是特例,它们应在不允许任何异常逃出环境的情况下完成。最后一句,小心引用类型交换,它可能引入大量潜在的BUG.
48 选择安全代码
.NET运行时被设计为防止恶意代码渗透并能在远程机器上执行。但是一些分布式系统信任从远程机器上下载并执行代码。如果你可能通过Internet网络或局部网传递你的软件,或直接从web运行你的软件,你需要了解CLR将强加在你程序集上的限制。如果CLR不完全信任一个程序集,它会限制允许的行为。这就叫代码访问安全(CAS).另一方面,CLR强制基于角色的安全,就是代码能或不能执行是基于特定的用户帐号权限。当你创建运行在浏览器中的Silverlight应用时,你会看到这些影响。浏览器模型在任何代码运行的环境里实施安全限制。
安全隐患是运行时的情况:编译器不能强制它们。此外,它们不太可能在你的开发机器上出现;并且你编译的代码从硬盘加载,因此有一个更高的信任级别。讨论所有的.NET安全模型的影响需要几卷书,但是你可以采取一小部分合理的动作,使你的程序集更容易的和.net安全模型进行交互.只有当你正在创建库组件或者会通过web传输的组件和程序时,这些建议才合适。
通过这些讨论,记住.NET是一个托管环境。该环境保障了一定程度的安全性。当安装.NET环境后,通过配置.NET策略,大部分的.NET框架库被完全信任。它确实是安全的,因为CLR能检查IL并确保它不能执行任何潜在的危险动作,如访问原始内存。对于访问本地资源,它不声明任何特殊安全权限。你应遵循同样的规则。如果你的代码不需要任何特殊安全权限,避免使用任何CAS的API来决定你的访问权限,所有你做的都只是在损伤性能。
你将会使用CAS的APS来访问一小部分要求提升权限的受保护的资源.最常见的受保护资源就是非托管内存和文件系统。其他的受保护资源包括数据库,网络端口,注册表,打印系统。对于每种情况,当调用代码没有合适的权限时,尝试访问这些资源将触发异常。进一步说,访问这些资源可能会引起运行时对安全栈进行遍历,以此来保证当前的调用栈里面的所有程序集都有合适的许可。让我们来看看内存和文件系统。你可以通过尽可能的创建验证性的安全程序集来避免访问非托管内存。一个安全的程序集就是不使用任何指针去访问托管的或者未托管的堆。无论你知不知道,几乎所有你创建的C#代码都是安全的,除非你打开了/unsafe编译选项。/unsafe允许你使用指针,CLR不会验证指针的安全性。
使用非安全代码的情况非常少,最常见的是在性能问题上使用非安全代码。指向原始内存的指针要比安全引用检查要快很多。在一个典型的数组中,能够快到10倍。但当你使用非安全结构,要理解程序集里面各处的非安全代码会影响整个程序集。当你创建非安全代码块,考虑将这些算法隔离在它们自己的程序集里(第50条)。这样可以限制非安全代码对你整个应用的影响。如果它被隔离了,只有需要该特定特性的调用者才会受影响。你仍然可以在更严格的环境下使用其余的安全功能。你也可以需要用非安全代码来处理P/Invoke或COM接口,这些都需要使用原始指针。对于这些使用同样的建议:隔离它。非安全代码应该只影响它自己的程序集而不是其他程序集。
对于内存访问的建议很简单:任何时候只要可能就避免访问非托管内存。当你需要访问非托管内存时,你应将访问隔离在一个单独的程序集里。
下一个普遍的安全问题是文件系统。程序存储数据,通常都存在文件里。来自互联网下载的代码不能有访问大多数文件系统位置的权利,那样将会是一个巨大的安全漏洞。但是,完全不访问文件系统的话又会很难去创建可用的程序。通过隔离存储可以解决这个问题。隔离存储可以被视为一个基于程序集的、应用域和当前用户的虚拟目录。你也可以使用一般的隔离存储目录,它基于程序集和当前用户。
部分受信任的程序集可以访问它们特定的隔离存储区域,但不能访问文件系统的任何其他地方。隔离存储目录对于其他程序集和用户是不可见的。通过使用System.IO.IsolatedStorage命名空间里的类来使用隔离存储。IsolatedStorageFile类包含的方法和System.IO.File类很相似。实际上,它是从System.IO.FileStream类派生出来的。写隔离存储的代码也几乎和写任何文件一样:
IsolatedStorageFile iso =IsolatedStorageFile.GetUserStoreForDomain();
IsolatedStorageFileStream myStream = new
IsolatedStorageFileStream("SavedStuff.txt",FileMode.Create, iso);
StreamWriter wr = new StreamWriter(myStream);
// several wr.Write statements elided
wr.Close();
读也和文件I/O非常相似:
IsolatedStorageFile isoStore =IsolatedStorageFile.GetUserStoreForDomain();
string[] files = isoStore.GetFileNames("SavedStuff.txt");
if (files.Length > 0)
{
StreamReader reader = new StreamReader(new
IsolatedStorageFileStream("SavedStuff.txt",FileMode.Open, isoStore));
// Several reader.ReadLines( ) calls elided.
reader.Close();
}
你可以使用隔离存储来保存合理大小的数据元素,使得部分受信任的代码可以从本地磁盘的仔细分区过的位置保存或者加载信息。.NET环境对每个应用程序的隔离存储的大小进行了限制。这样可以阻止恶意代码消耗过多的磁盘空间,最终导致系统不可用。隔离存储对于其他程序集和用户是不可见的,因此对于管理员可能需要进行维护的部署或者配置,不应这样使用。但是即使它是隐藏的,隔离存储对于非托管代码或者信任的用户是不受保护的。除非你使用了额外的加密,否则请不要为高度机密使用隔离存储。
为了创建一个在文件系统中存在于安全限制内的程序集,隔离你的存储流创建吧。当你的程序集可能运行在web或可能被运行在web上的代码访问时,考虑隔离存储。
你可能也需要其他的保护资源。一般来说,访问这些资源意味着你的程序需要被完全被信任。唯一的选择就是完全避免使用这些受保护的资源。如:考虑使用Windows注册表。如果你的程序需要访问注册表,你必须在终端用户的电脑上安装你的程序,这样它才有访问注册表的权限。你不能安全的创建一个从web里运行的注册表编辑器。
.NET安全模型意味着你的程序的行为要根据它的权限来被检查。留意你的程序需要的权限,并尝试使其最小化。不要请求你不需要的权限。你的程序集需要的保护资源越少,它生成安全异常的可能性就越小。当你为了某些算法需要更高级别的安全权限时,隔离那些代码在它们自己的程序里。
49 选择CLS兼容的程序集
.NET环境是与语言无关的:开发者可以用各种不同的语言编写组件而不受限制。实际开发中也几乎是这样。你创建的程序集必须是与公共语言系统(CLS)是兼容的,这样才能保证其它的开发人员可以用其它的语言来使用你的组件。
C#的一个优势是因为它设计是运行在CLR上的,那么几乎所有你的C#程序集都是CLS兼容的。但许多其他的语言不一定是这样。很多F#构造的不能编译到与CLS兼容的类型。DLR语言,如IronPython和IronRuby,不能创建与CLS兼容的程序集。这也是为什么C#是.NET中组件开发的一个很好的选择。C#组件可以被运行在CLR上的所有语言使用。因为它是CLS兼容的。
遵循CLS是在互操作性的公共标准上一个新的扭转。CLS规范是所有语言必须支持的操作的一个子集。为了创建一个CLS兼容的程序集,你必须创建一个公共接口限制在CLS规范里的特性的程序集。这样任何支持CLS规范的语言就能使用这个组件。这并不是说你必须把你的整个程序限制在符合CLS兼容的C#语言子集。
为了创建CLS兼容的程序集,你必须遵从两个规则.第一,所有参数的类型和公共、保护成员的返回值都必须兼容CLS.第二,任何非CLS兼容的公共、保护成员必须有一个CLS兼容的同义词。
第一个规则很容易实现:你可以让编译器来强制完成。在你的程序集上增加CLSCompliant属性就可以了。
[assembly: System.CLSCompliant(true)]
编译器强制整个程序集都遵从CLS。如果你写的一个公共方法或属性使用了一个非CLS兼容的构造,编译器会认为这是错误的。这很不错,因为它让遵循CLS成了一个简单的目的。在打开与CLS兼容性后,下面两个定义将不能通过编译,因为无符号整型与CLS不兼容。
// Not CLS Compliant, returns unsigned int:
public UInt32 Foo()
{
return foo;
}
// Not CLS compliant, parameter is an unsigned int.
public void Foo2(UInt32 parm)
{
}
记住创建与CLS兼容的程序集时,只对那些可以在当前程序集外面可以访问的内容有效。当Foo和Foo2声明为公共或保护类型时,会产生CLS兼容错误。但如果Foo和Foo2是内部或私有的,它们就能被包含在CLS兼容的程序集里。CLS兼容接口仅在被那些暴露在外面的程序集的内容所需要。
属性呢?它是CLS兼容的吗?public MyClass TheProperty { get; set; }
它看情况而定。如果MyClass是CLS兼容的,而且表明了它是与CLS兼容的,那这个属性也是CLS兼容的。反之,如果MyClass没有标记为与CLS兼容,这个属性也是与CLS不兼容的。这意味着前面的TheProperty属性只有在MyClass是在与CLS兼容的程序集中时,它才是与CLS兼容的。
如果在你的公共或保护的接口里有类型不是CLS兼容的,你将不能建立一个CLS兼容的程序集。作为一个组件的设计者,如果你没有给程序集标记为CLS兼容的,那么对于你的用户来说,就很难创建与CLS兼容的程序集了。他们必须隐藏你的类型,然后在CLS兼容的封装器里映射功能。确实,这样可以完成任务。但是,对于那些使用组件的程序员来说这不是一个好方法。最好还是你来努力完成所有工作,来让程序与CLS兼容。对于用户要在他们CLS兼容的程序集里包含你的工作,这是最简单的方式。
第二个规则由你决定:你需要确保所有公共和受保护的操作都是与语言无关的。你也需要确保你所使用的多态接口中没有偷偷地使用不兼容的对象。
操作符重载是一个让人又爱又恨的特性。同样地,也并不是所有的语言都支持操作符重载的。CLS标准对于重载操作符这一概念既没赞成也没反对。取而代之,它为每个操作符定义了一个函数:op_equals就是=所对应的函数名.op_add是重载了加号后的函数名.当你重载了操作符以后,操作符语法就可以在支持操作符重载的语言中使用.开发者使用不支持操作符重载的语言时,必须使用op_这样的函数名。如果你希望这些程序都使用你的CLS兼容程序集,你应该创建更多的方便的语法。这样就有一个简单的建议::任何时候你重载一个运算符操作时,再创建一个等效的函数。
// Overloaded Addition operator, preferred C# syntax:
public static Foo operator +(Foo left, Foo right)
{
// Use the same implementation as the Add method:
return Foo.Add(left, right);
}
// Static function, desirable for some languages:
public static Foo Add(Foo left, Foo right)
{
return new Foo(left.Bar + right.Bar);
}
最后,注意当你在使用多态的接口时,非CLS的类型可能隐藏在一些接口中。 这很容易出现事件参数中。你创建了一个与CLS不兼容的类型,然后使用它的地方又是一个与CLS兼容的基类类型。
假设你创建一个EventArgs的派生类:
public class BadEventArgs : EventArgs
{
public UInt32 ErrorCode;
}
BadEventArgs类型是与CLS不兼容的;你不应在其它语言写的事件句柄上使用这个类。但是多态可能会让这个很容易发生。你可以使用基类来声明事件类型,EventArgs:
// Hiding the non-compliant event argument:
public delegate void MyEventHandler(object sender, EventArgs args );
public event MyEventHandler OnStuffHappens;
// Code to raise Event:
BadEventArgs arg = new BadEventArgs();
arg.ErrorCode = 24;
// Interface is legal, runtime type is not:
OnStuffHappens(this, arg);
接口声明使用了EventArgs参数,它是CLS兼容的。但是,实际取代参数的类型却是与CLS不兼容的。结果就是一些语言不能使用这个。尝试使用这些类型的开发者将不能调用你的程序集的方法。他们的语言甚至可能隐藏了这些API的显示。或者他们可以看到API的显示但不能访问它们。
最后以如何实现CLS兼容类或者不兼容接口来结束这条的讨论。它可能是很复杂的,但我们希望它是简单的。明白CLS与接口的兼容同样可以帮助你完整的理解CLS兼容的意思,而且可以知道运行环境是怎样看待兼容的。
这个接口如果是定义在CLS兼容程序集中,那么它是CLS兼容的:
[assembly: CLSCompliant(true)]
public interface IFoo
{
void DoStuff(Int32 arg1, string arg2);
}
你可以在任何与CLS兼容的类中实现它。但是,如果你在与没有标记与CLS兼容的程序集中定义了这个接口,那么这个IFoo接口就并不是CLS兼容的接口。换句话说,一个接口只是满足CLS规范是不够的,还必须定义在一个CSL兼容的程序集中时才是CLS兼容的.原因是编译器造成的,编译器只在程序集标记为CLS兼容时才检测CLS兼容类型。类似的,编译器总是假设在CLS不兼容的程序集中定义的类型实际上都是CLS不兼容的。但是,这个接口的成员具有CLS兼容性标记。甚至如果IFoo没有CLS兼容标记,你也能在CLS兼容类中实现这个IFoo接口。这个类的客户可以通过类的引来访问DoStuff,而不是IFoo接口的引用。
考虑这个简单的变动:
public interface IFoo2
{
// Non-CLS compliant, Unsigned int
void DoStuff(UInt32 arg1, string arg2);
}
一个公共的实现了IFoo2的类是与CLS不兼容的。为了使类实现IFoo2,也是与CLS兼容的,你必须使用显示接口实现:
public class MyClass2 : IFoo2
{
// explicit interface implementation.
// DoStuff() is not part of MyClass‘s public interface
void IFoo2.DoStuff(UInt32 arg1, string arg2)
{
// content elided.
}
}
MyClass有一个与CLS兼容的公共接口。期望访问IFoo2 接口的客户必须通过访问与CLS不兼容的IFoo2接口指针。
复杂吗?不,一点也不复杂。创建一个CLS兼容的类型仅要求你的公共接口包含CLS兼容类型。 这就是说你的基类必须是CLS兼容的。所有你实现的公共接口也必须是CLS兼容的。如果你实现了一个非CLS兼容的接口,你必须使用显示接口实现来在你的公共接口里隐藏它。
遵循CLS并不强迫你去使用最小的公共名称来实现你的设计。它只是告诉你应该小心使用程序集上的公共的接口。对于任何的公共或受保护的类,在这些构造函数中提到的任何类型必须是CLS兼容的:
基类.
从公共或者受保护的方法和属性上返回的值.
公共及受保护的方法的参数和索引器.
运行时事件参数.
已声明的或已实现的公共接口.
编译器会尝试强制兼容一个程序集.这会让你提供最小级别上的CLS兼容支持变得很容易.再多加小心点,你就可以创建一个其它语言都可以使用的程序集了.CLS规范尝试确保不用牺牲你所喜欢的语言的结构就可以完成互操作性。你只用在接口中提供可选的方案就行了。
CLS兼容性要求你花点时间站在其它语言的立场上来考虑一下公共接口。你不必限制所有的代码都与CLS兼容,只用避免接口中的不兼容结构就行了。中间语言的可操作性值得你花点时间。
50 选择更小的,内聚的程序集
这条真的标题应该是"构建正确大小的并包含少数公共类型的程序集".但是那样标题太长了,所以我以我看到的最常见的错误来进行命名:开发者几乎把所有的东西都放在一个程序集里面.那样会使组件复用变得很困难,而且很难去更新系统的一部分。很多小的程序集使得将你的类作为二进制组件更容易。
该标题也显著的说明了内聚的重要性.内聚是一个度,这个度负责把单个组件形成一个有意义的单元。内聚组件可以以一个简单的语句来描述。你可以在很多.NET FCL程序集里看到内聚。其中两个例子是:System.Core程序集提供支持LINQ的类型和算法,System.Windows.Forms程序集支持Windows控制模型的类。Web窗体和Windows窗体在不同的程序集,因为它们是不相关的。你应该能够像这样使用一个简单的语句来描述你自己的程序集。不欺骗的:MyApplication程序集提供所有你需要的。是的,那是一个简单的句子。但它也太懒了,你很可能不需要My2ndApplication里面所有的功能(虽然你可能喜欢重用其中一些。这"其中的一些"应是处于自己程序集中的包.)
你不应创建只有一个公共类的程序集。你需要找到一个折衷。如果你走的太偏激而创建太多程序集,就失去了一些封装性的好处:你就失去了内部类型的好处,因为你没有在同一个程序集里面打包相关联的类。在一个程序集内部与跨程序集相比较,JIT编译器能执行更有效率的内联。这意味着,在同样的程序集里打包相关类型是对你有利的。你的目标是为在你的组件中传递的功能创建大小最合适的包。使用内聚组件这个目标很容易达到:每个组件都应有它的职责。
在某种意义上,一个程序集就是一个二进制的类。我们使用这些类来封装算法和存储数据。只有公共类、结构、接口才是正式契约的一部分,所以只有公共类型对用户才是可见的。(记住接口不能声明为protected)在同样意义上,程序集为一系列相关集合的类提供了一个二进制包。在程序集外部只能看到公共的和受保护的类。实用工具类可以是内部的程序集。是的,它们比私有内嵌类有更高的可见性,但通过某种机制,你可以不向这些类的所有用户暴露具体实现就能在程序集内部共享一些通用的实现。将你的应用分成多个程序集,并在单独的包里封装相关的类型。
拆分功能到程序集意味着比你像第几条建议一样的短文有更多的代码。但不是写一个完整的新应用,我将为来自第44条的动态CSV类讨论一个多样的增强。你需要决定如果新特性属于你已经交付的核心能力,或如果它是你的用户欣赏的一小部分选项。我创建的版本在CSV文件中以字符串方式返回所有数据。你可以创建适配器,当列支持它的时候,它将转换字符串到数值。那可能也是大多数用户想要的。这些适配器应该在同样的程序集里。另外的添加可能支持多级标题。那将启用嵌套标题,就像Excel的透视表。那感觉就像你将一些东西放入不同的程序集。只有你的一些用户会使用这个特性。最常见的用法还是包含一个标题的版本。那意味着把多个标题的功能放入不同的程序集也是最有意义的。它可能依赖于核心程序集,但它不应在相同的位置。
国际化呢?没有一个简单的答案。你可能为不同国家的企业创建应用,多语言支持对每个人来说都是重要的。或者,你可以为局部的足球联盟写一个简单的工具。或者你希望读者是任何地方的。如果你的大多数用户都是一种语言,不管什么可能,分离多个语言到单独的程序集(或甚至每个语言一个程序集)是有意义的。另一方面,如果你的用户经常需要以不同的语言使用CSV文件,那多语言应作为核心功能的一部分。你需要决定是否这个新功能将是有用的,对于你的核心功能的绝大多数用户来说。如果是,你应增加这个新功能到这个相同的程序集。但是,如果这个新功能仅被希望使用在一些更复杂的例子里,你应分离那些功能到一个单独的可传递单元。
第二,使用多程序集使得不同的部署选项更容易。考虑一个三层架构的应用,一部分运行在智能客户端,一部分运行在服务器.在客户端上你支持一些验证规则,这样当用户进入或者编辑数据的时候会得到反馈。你在服务端重复这些规则并将它们与其他规则一起来提供更健壮的验证。在服务端实现完整的业务规则,只有一些子集在每个客户端维护。
当然,你能重用这些源码并为客户端和服务端的业务规则创建不同的程序集,但那样将使你的发布机制复杂化。你在更新规则时,有两个不同的版本要编译和两个安装程序要执行.相反,应该通过将它们放置在不同的程序集里面,将客户端验证和更健壮的服务器端验证相分离.你是重用打包在程序集里的二进制对象,而不是重用对象代码或源代码,再编译这些对象到不同的程序集里。
一个程序集应包含一个已组织的相关功能的库。那是易说难做的。现实情况是你可能事先不知道哪个类会被同时部署在服务器和客户端上。更有可能,服务端和客户端的功能集合都有些不确定。你将在这两者之间移动特性。通过保持最小的程序集,在服务器端和客户端上进行重新部署的时候会更容易。对于你的应用来说程序集是一个二进制编译块。那样向一个正在工作的应用程序插入一个新的组件将会很容易。如果你制造了一个错误,生成很多小程序集比一些大的程序集也要好处理。
我经常使用Legos作为程序集和二进制组件的一种类比。你可以拔出一个Lego并很容易地替换它;它是一个块。同样的,你应能够抽出一个程序集,使用另外一个有同样接口的程序集来替换它。应用的其他部分应像什么都没发生一样继续工作。跟着Lego再深入一点,如果所有你的参数和返回值都是接口,那么任何一个程序集都可以被其它实现了同样接口的程序集替换掉.(第22条)
越小的程序集也让你应用的启动开销分散开来。一个程序集越大,加载程序集的时候CPU就做越多的工作,并转换必要的IL到机器指令。只有日常的启动调用被JIT编译,但是整个程序集都被加载,CLR会为程序集的每个方法创建存档。
是时候休息一下了,确保我们没有走向极端。该条是关于要确保你不会创建单块的程序,而是要构建二进制系统,可重用的组件。你可能带着这个建议走得太远。一个有太多小程序集的大程序会有一些性能代价。当程序流在程序集之间流动时,将可能招致性能的惩罚。为了加载这么多的程序集,并且将IL转换成机器指令,尤其是解析函数地址,这将会使CLR加载器有点多的工作。
额外的跨程序集的安全检查也会执行。所有从同样程序集来的代码有相同的信任级别。(不一定是有相同的访问权限,但是有相同的信任级别).无论何时当代码流跨越程序集边界时,CLR会执行一些安全检查。你的程序跨程序集越少,它的效率将越高。
任何一个这些性能上的关注都不应阻止你对很大的程序集进行分隔。性能损失是较小的。C#和.NET设计时就考虑到组件,为了较大的灵活性通常也值得付出这些性能上的代价。
那么,如何来决定该有多少代码或者该有多少类应该在一个程序集里面呢?更重要的,如何决定哪个代码在哪个程序集里呢?很大程度上这取决于特定的应用程序,所以这里没有标准答案。我的建议是:从查看你的所有公共类开始。合并通用基类的公共类到一个程序集。然后在相同的程序集添加必要的工具类,并提供所有相关功能的公共类。打包相关的公共接口到它们自己的程序集。最后一步,查找横穿你的应用的类,它们是一个广泛的实用程序集的候选者,这个实用程序集包含了你的应用的实用库。
最后的结果是,你创建了一个组件,包含了一个单独的相关集合的公共类,并且实用类必须去支持它。你创建了一个足够小的程序集,来获得易更新易重用的好处,同时仍最小化多个程序集带来开销。精心设计的内聚组件可以以一句简单的话来描述。如:"Common.Storage.dll管理离线数据缓存和所有用户设置"描述了一个低内聚的组件。相反,生成两个组件:"Common.Data.dll管理离线数据缓存,Common.Settings.dll管理用户设置".当你将它们分开后,你可能需要第三个组件:"Common.EncryptedStorage.dll为加密的本地存储管理文件IO"。你可以单独地更新这三个组件中的任一个。
小是一个相对的词语。mscorlib.dll大概有2MB;System.Web.RegularExpressions.dll仅有56KB.它们都满足小又重用的程序集的核心设计目标:它们包含一系列相关的类和接口。完全的大小和功能的不同有关:mscorlib.dll包含所有你每个应用都需要的底层的类。System.Web.RegularExpressions.dll非常特殊。它仅包含那些需要在Web控件中支持正则表达式的类。你将创建2种类型的组件:关注某个特定功能的小的程序集,包含通用功能的广泛的程序集。无论哪种情况,都要让他们小到合理,而不是一味的追求小。
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。