C# 中的防御性编程(带 T4)






4.50/5 (5投票s)
本文旨在提供一个简单的库,该库尝试解决一些防御性问题,重点关注如何以安全和明确的方式管理函数返回值。
引言
本文旨在提供一个简单的库,该库尝试解决一些防御性问题,重点关注如何以安全和明确的方式管理函数返回值。
背景
C# 和 T4 的基本概念。修复无迹可寻的错误和编写弹性代码的糟糕经历。
问题概述
关于防御性编程有很多文档。其中一些只是简短的句子,比如“我正在保护我自己的代码不受我自己的影响”或“永远不要假设任何事情”,这些都很好,但在某些行业领域,防御性编程有更严格的定义。例如,铁路行业使用 CENELEC 50128 来推动软件开发达到尽可能高的标准,因为他们通常管理的问题的敏感性:人类生命。
CENELEC 50128 定义了为满足软件需求规范中定义的安全级别而必须实施的流程和技术。对于更高的安全级别(SIL 4,安全完整性级别 4),必须应用防御性编程,并且在 B.15 节中列出了一些技术。
- 变量应进行范围检查。
- 在可能的情况下,应检查值的合理性。
- 过程的参数应在过程入口处进行类型、维度和范围检查。
- 具有物理意义的输入变量和中间变量应进行合理性检查。
- 应检查输出变量的效果,最好通过直接观察相关系统状态变化进行。
正如每个人都可以想象的那样,所有这些代码流控制都不容易,不便宜,也不快(我指的是计算时间和开发人员的精力)。在我看来,防御性编程是基于对参数传递的偏执概念:一个方法/类不能信任调用者,不能信任被调用者,甚至可能不应该信任自己。通常,防御性和偏执是联系在一起的词,过于偏执意味着代码过多或无用代码,过于放松意味着错误,这一切都取决于对代码的控制程度。还有一些我(不仅仅是我)认为危险的编码方式。
- 通过引用传递参数。
- 使用特殊值来指出函数出现错误(例如:null,-1)。
- 在返回函数对象中使用可写字段。
这三点是本文的核心。我想对一些词的含义再做一次详细说明。我在在线词典中发现 safe 和 secure 之间的区别是:
- safe 是没有风险的事物。拉丁语:salvus -> 健全,完整。
- secure 是处于某物/某人照看下的事物/某人。拉丁语:securus -> 安静,无惧。
另一方面,CENLEC 50128 第 3.21 段对 safety 的定义是:免于不可接受的风险水平。因此,对我来说,在谈论函数返回值时使用“safe”这个词更好。
在简要介绍之后,是时候展示代码了。
1. 字符串
/// <summary>
///
/// </summary>
public abstract class ReturnValueAC
{
private Boolean priSuccess = false;
public void SetFalse()
{
priSuccess = false;
}
public void SetSuccess(Boolean success)
{
priSuccess = success;
}
public Boolean Success
{
get {return priSuccess;}
}
}
/// <summary>
///
/// </summary>
public class StringReturnValue : ReturnValueAC
{
public StringReturnValue()
{
}
public StringReturnValue(String outcome , Boolean success)
{
if (success == true)
{
this.Outcome = outcome;
SetSuccess(success);
}
}
public readonly String Outcome;
}
关于这两个类没有太多可说的。一个抽象类,其中包含一个只读属性(还记得防御性建议吗?),它暴露了一个函数是否以一致的结果退出。一个具体类专门用于返回函数的结果,在这种情况下是 String 类型。我使用抽象类和具体类的原因将在 T4 部分中更清楚,该部分将展示以非常简单的方式生成代码的能力。
这些 ReturnValue
类可以这样使用:
public class DefensiveClass
{
public DefensiveClass()
{
RunSafe();
RunUnSafe();
}
/// <summary>
/// Remove from a string a substring
/// E1) If substring is no found then error
/// E2) If one of parameters are empty or null then error
/// </summary>
/// <param name="parThis"></param>
/// <param name="parThat"></param>
/// <returns></returns>
private StringReturnValue GetThisWithoutThat(String parThis, String parThat)
{
StringReturnValue ReturnValue = new StringReturnValue();
if((String.IsNullOrEmpty(parThis) == false)
&&
(String.IsNullOrEmpty(parThat) == false))
{
String sReplace = parThis.Replace(parThat, String.Empty);
if (sReplace != parThis)
{
ReturnValue = new StringReturnValue(sReplace,true);
}
else
{
// E1
ReturnValue.SetFalse();
}
}
else
{
// E2
ReturnValue.SetFalse();
}
return ReturnValue;
}
private void RunSafe()
{
StringReturnValue srv1 = GetThisWithoutThat("ClassMethod", "Method");
StringReturnValue srv2 = GetThisWithoutThat("Class", "Method");
String sFormat = "{0} Success = {1} , Outcome = {2}";
String sSomethingGoesWrong = "[Do not care about it , it has no meaning]";
String sMessage = String.Empty;
if (srv1.Success == true)
{
sMessage = String.Format(sFormat, "srv1", srv1.Success, srv1.Outcome);
}
else
{
sMessage = String.Format(sFormat, "srv1", srv1.Success, sSomethingGoesWrong);
}
Console.WriteLine(sMessage);
if (srv2.Success == true)
{
sMessage = String.Format(sFormat, "srv2", srv2.Success, srv1.Outcome);
}
else
{
sMessage = String.Format(sFormat, "srv2", srv2.Success, sSomethingGoesWrong);
}
Console.WriteLine(sMessage);
}
}
第一次调用方法没有问题。
StringReturnValue srv1 = GetThisWithoutThat("ClassMethod", "Method");
srv1 Success = True , Outcome = Class
但第二次出错了。
StringReturnValue srv2 = GetThisWithoutThat("Class", "Method");
并打印此消息:
srv2 Success = False , Outcome = [Do not care about it , it has no meaning]
如果我使用一个简单地进行替换并返回 String 的函数,并检查注释中描述的相同约束,它会返回什么?
- 返回 null 值是可能的,但在防御性编程中应避免,因为我不信任调用者,并且如果管理不当,它可能简单地引发异常。记住 Finagle 定律:“任何可能出错的事情,都将在最糟糕的时候出错。”而且我宁愿不让 null 在代码中自由运行。
- 它可能返回 "",但这到底意味着什么?我们存在歧义,因为即使是两个相等的字符串也会返回 "",这不是一个错误。
- 简单地返回“Class”,这是另一个歧义。
- 特殊值,例如 = “ERROR”。如果 parThis = “ERRORMethod”,我们就有问题了。
以下方法显示了我们需要做多少额外的工作,以更少的价值来管理相同的情况。
enum unSafeCases
{
CASE1,
CASE2,
CASE3,
CASE4
}
/// <summary>
/// Remove from a string a substring
/// E1) If substring is no found then error
/// E2) If one of parameters are empty or null then error
/// </summary>
/// <param name="parThis"></param>
/// <param name="parThat"></param>
/// <returns></returns>
private String GetThisWithoutThat(String parThis, String parThat,unSafeCases unSafe)
{
String ReturnValue = String.Empty;
String sReplace = String.Empty;
if( ( String.IsNullOrEmpty(parThis) == false) &&
( String.IsNullOrEmpty(parThat) == false) )
{
sReplace = parThis.Replace(parThat, String.Empty);
if (sReplace != parThis)
{
// If it's all ok return the same value but ...
ReturnValue = sReplace;
}
else
{
// E1
switch(unSafe)
{
case unSafeCases.CASE1:
// Case 1
ReturnValue = null;
break;
case unSafeCases.CASE2:
// Case 2
ReturnValue = String.Empty;
break;
case unSafeCases.CASE3:
// Case 3
ReturnValue = sReplace;
break;
case unSafeCases.CASE4:
// Case 4
ReturnValue = "ERROR"; // Or better "ERROR(1)"
break;
}
}
}
else
{
// E2
switch(unSafe)
{
case unSafeCases.CASE1:
// Case 1
ReturnValue = null;
break;
case unSafeCases.CASE2:
// Case 2
ReturnValue = String.Empty;
break;
case unSafeCases.CASE3:
// Case 3
ReturnValue = sReplace;
break;
case unSafeCases.CASE4:
// Case 4
ReturnValue = "ERROR"; // Or better "ERROR(2)"
break;
}
}
return ReturnValue;
}
private void RunUnSafe()
{
String sCase1_wrong = GetThisWithoutThat("Class", "Method", unSafeCases.CASE1);
if (sCase1_wrong == null)
{
Console.WriteLine("Case1 is null");
}
else
{
//
}
String sCase2_wrong = GetThisWithoutThat("Class", "Method", unSafeCases.CASE2);
String sCase2_right = GetThisWithoutThat("Class", "Class", unSafeCases.CASE2);
if (sCase2_wrong == sCase2_right)
{
Console.WriteLine("Case2 : Who's right ? Who's wrong ?");
}
else
{
//
}
String sCase3_wrong = GetThisWithoutThat("Class", "Method", unSafeCases.CASE3);
String sCase3_right = GetThisWithoutThat("ClassMethod", "Method", unSafeCases.CASE3);
if (sCase3_wrong == sCase3_right)
{
Console.WriteLine("Case3 : Who's right ? Who's wrong ?");
}
else
{
//
}
String sCase4_wrong = GetThisWithoutThat("Class", "Method", unSafeCases.CASE4);
String sCase4_right = GetThisWithoutThat("ERRORMethod", "Method", unSafeCases.CASE4);
if (sCase4_wrong == sCase4_right)
{
Console.WriteLine("Case4 : Who's right ? Who's wrong ?");
}
else
{
//
}
}
完整的输出是:
srv1 Success = True , Outcome = Class
srv2 Success = False , Outcome = [Do not care about it , it has no meaning]
Case1 is null
Case2 : Who's right ? Who's wrong ?
Case3 : Who's right ? Who's wrong ?
Case4 : Who's right ? Who's wrong ?
通过这个围绕原始 String 类的简单包装器,可以管理返回值,并确信返回代码中没有歧义,一个值是关于该方法中发生的操作的结果,另一个值是关于操作是否根据其边界/约束完成。字段 Success 必须被视为一个标记,用于证明 Outcome 是否具有有价值的值,通过这种方式,调用方法会收到警告,不要使用 Outcome,同时调用者不必知道特殊值来管理被调用方法中的错误。
2. 整数
当我们使用一些特殊值来指出一个返回整数的函数内部出错了,会发生什么?以下代码展示了如何以安全的方式管理简单 mean() 函数的返回值。
public class DefensiveClassInteger
{
int[] arr1_wrong = null;
int[] arr2_wrong = new int[] { };
int[] arr3_wrong = new int[] { int.MaxValue, int.MaxValue };
int[] arr1_right = new int[] { 3, -3 };
int[] arr2_right = new int[] { 0, -2 };
int[] arr3_right = new int[] { 0, -4 };
public DefensiveClassInteger()
{
Console.WriteLine("Begin of Safe code");
RunSafe();
Console.WriteLine("End of Safe code");
Console.WriteLine("Begin of UnSafe code");
RunUnSafe();
Console.WriteLine("End of UnSafe code");
}
private void Print(String message, Boolean AreUnequal )
{
if (AreUnequal == true)
{
Console.WriteLine(String.Format("{0} , Right is different from Wrong ! Do you expect something else?",message));
}
else
{
Console.WriteLine(String.Format("{0} , Confusion in progress!!",message));
}
}
private void RunSafe()
{
intReturnValue irv1_wrong = MeanSafeEdition(arr1_wrong);
intReturnValue irv1_right = MeanSafeEdition(arr1_right);
Print("irv1",irv1_wrong.Success != irv1_right.Success);
intReturnValue irv2_wrong = MeanSafeEdition(arr2_wrong);
intReturnValue irv2_right = MeanSafeEdition(arr2_right);
Print("irv2", irv2_wrong.Success != irv2_right.Success);
intReturnValue irv3_wrong = MeanSafeEdition(arr3_wrong);
intReturnValue irv3_right = MeanSafeEdition(arr3_right);
Print("irv3", irv3_wrong.Success != irv3_right.Success);
}
private void RunUnSafe()
{
int irv1_wrong = MeanUnSafeEdition(arr1_wrong);
int irv1_right = MeanUnSafeEdition(arr1_right);
Print("irv1", irv1_wrong != irv1_right);
int irv2_wrong = MeanUnSafeEdition(arr2_wrong);
int irv2_right = MeanUnSafeEdition(arr2_right);
Print("irv2", irv2_wrong != irv2_right);
int irv3_wrong = MeanUnSafeEdition(arr3_wrong);
int irv3_right = MeanUnSafeEdition(arr3_right);
Print("irv3", irv3_wrong != irv3_right);
}
/// <summary>
/// Return the mean of a array of int
/// </summary>
/// <param name="arrayOfInt"></param>
/// <returns>ReturnValue.Success = true => ReturnValue.Outcome has a worthwhile value</returns>
public intReturnValue MeanSafeEdition(int[] arrayOfInt)
{
intReturnValue ReturnValue = new intReturnValue();
try
{
int iCounter = 0;
if (arrayOfInt != null)
{
if (arrayOfInt.Length > 0 )
{
for (int iIndex = 0; iIndex < arrayOfInt.Length; iIndex++)
{
// Why using "cheched" ?
// See :
// http://msdn.microsoft.com/en-us/library/system.overflowexception.aspx
// http://msdn.microsoft.com/en-us/library/6a71f45d.aspx
// http://msdn.microsoft.com/en-us/library/khy08726.aspx
iCounter = checked(iCounter + arrayOfInt[iIndex]);
}
ReturnValue = new intReturnValue(iCounter / arrayOfInt.Length, true);
}
else
{
ReturnValue.SetFalse();
}
}
else
{
ReturnValue.SetFalse();
}
}
catch (Exception ex)
{
// OverflowException for example
ReturnValue.SetFalse();
}
return ReturnValue;
}
/// <summary>
/// Return the mean of a array of int
/// </summary>
/// <param name="arrayOfInt"></param>
/// <returns>ReturnValue = ??? </returns>
public int MeanUnSafeEdition(int[] arrayOfInt)
{
int ReturnValue = 0;
try
{
int iCounter = 0;
if (arrayOfInt != null)
{
if (arrayOfInt.Length > 0)
{
for (int iIndex = 0; iIndex < arrayOfInt.Length; iIndex++)
{
// Why using "cheched" ?
// See :
// http://msdn.microsoft.com/en-us/library/system.overflowexception.aspx
// http://msdn.microsoft.com/en-us/library/6a71f45d.aspx
// http://msdn.microsoft.com/en-us/library/khy08726.aspx
iCounter = checked(iCounter + arrayOfInt[iIndex]);
}
ReturnValue = iCounter / arrayOfInt.Length;
}
else
{
// It's source of confusion
ReturnValue = -1;
}
}
else
{
// It's source of confusion
ReturnValue = 0;
}
}
catch (Exception ex)
{
// OverflowException for example
// It's source of confusion ( of other kind)
ReturnValue = -2;
}
return ReturnValue;
}
}
控制台中的输出如下:
Begin of Safe code
irv1 , Right is different from Wrong ! Do you expect something else?
irv2 , Right is different from Wrong ! Do you expect something else?
irv3 , Right is different from Wrong ! Do you expect something else?
End of Safe code
Begin of UnSafe code
irv1 , Confusion in progress!!
irv2 , Confusion in progress!!
irv3 , Confusion in progress!!
End of UnSafe code
这里,一个模棱两可的返回值可能造成的潜在损害显而易见。在互联网上进行简短搜索后,很容易找到大量例子,其中一个简单的非安全 mean()
函数会产生歧义。好吧,有人可能会说 mean()
函数必须返回 double/decimal,但问题将是相同的,我们需要一个 DoubleReturnValue
来避免歧义。
3. T4 工作原理
现在使用 T4 可以为 C# 中的其他原始类型和 List 编写类。T4,也就是 Text Template Transformation Toolkit,是一个编写重复代码的强大工具,这里是开始开发实现一些防御性问题的自定义类的代码。
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>
<# String[] ClassNames = new string [] {"String",
"int",
"Int16",
"UInt16",
"Int32",
"UInt32",
"Int64",
"UInt64",
"Byte",
"SByte",
"Single",
"Double",
"Char",
"Boolean",
"Object",
"Decimal"
}; #>
// Return Value
using System;
using System.Collections.Generic;
namespace ReturnValueN
{
public abstract class ReturnValueAC
{
protected Boolean proSuccess = false;
public void SetFalse()
{
proSuccess = false;
}
public Boolean Success
{
get {return proSuccess;}
}
}
namespace PrimitivesN
{
<#
foreach (String classname in ClassNames)
{
#>
/// <summary>
///
/// </summary>
public class <#= classname #>ReturnValue : ReturnValueAC
{
private <#= classname #> priOutcome = default(<#= classname #>);
public <#= classname #>ReturnValue()
{
}
public <#= classname #>ReturnValue(<#= classname #> outcome , Boolean success)
{
priOutcome = outcome;
proSuccess = success;
}
public <#= classname #> Outcome
{
get {return priOutcome;}
}
}
<#
}
#>
} // End Namespace PrimitivesN
namespace ListOfN
{
<#
foreach (String classname in ClassNames)
{
#>
/// <summary>
///
/// </summary>
public class ListOf<#= classname #>ReturnValue : ReturnValueAC
{
private List<<#= classname #>> priOutcome = new List<<#= classname #>>();
public ListOf<#= classname #>ReturnValue()
{
}
public ListOf<#= classname #>ReturnValue(List<<#= classname #>> outcome , Boolean success)
{
priOutcome = outcome;
proSuccess = success;
}
public List<<#= classname #>> Outcome
{
get {return priOutcome;}
}
}
<#
}
#>
} // End Namespace PrimitivesN
} // End of namespace
4. 更新建议
上述 T4 代码包含一个用于编写 List 命名空间的模板,但没有任何东西禁止您添加一些功能。这里我提出一些建议:
- Array 的命名空间
- 其他 .NET 类的命名空间(System.IO.File、XML 等)
- 带日志或时间戳的类。当出现问题时,最好知道是谁做的,或者在哪里/何时发生的。
5. 阅读
当我开始写这篇文章时,我认为添加一章关于有趣阅读的内容是个好主意,以便学习如何做、不做什么等等。但在谷歌搜索(例如“防御性编程”)之后,我发现的材料如此之多,以至于很难选择一些既不太特定于语言,又不太学术化的内容。所以我决定做一个总结,而不是写一个很长的网页列表。
- 检查输入参数的范围和类型:断言、契约式编程、特定函数等。
- 检查输出参数的范围和类型:断言、契约式编程、特定函数等。
- 尽早检查,经常检查。
- 降低复杂性:例如,我更喜欢使用临时变量。
if ( (a + b < c) || ( b < a ) || ( a > 0) )
{
// do something
}
Can be rewritten in :
Boolean bCheck1 = (a + b < c) ;
Boolean bCheck2 = ( b < a ) ;
Boolean bCheck3 = ( a > 0) ;
Boolean bCheckAll = bCheck1 || bCheck2 || bCheck3 ;
if(bCheckAll == true)
{
// do something
}
这样更容易调试。一般来说,在编写代码时,总是要考虑到它需要被调试。不要假设它会在第一次尝试时就成功。
- 源代码审查:允许其他人阅读代码。他们会更快地发现错误。
- 测试它(自动化测试更好)。
- 将其重用于其他项目,如果一切正常,您就做了一项出色的工作,否则您会发现错误,因为代码内部的不同路径会产生不同的结果。修复它并继续。
6. 结论
本文专门探讨了防御性编程中函数返回值的问题,并辅以 T4 等工具。其目的是就如何管理典型的编程问题提供一些技巧和建议。它对我帮助很大,希望也能帮助读者。
历史
初版:2014 年 5 月 31 日