如果你必须为防御性编码选择你最喜欢的(聪明的)技术,它们会是什么?虽然我目前的语言是Java和Objective-C(有C++背景),但可以随意用任何语言回答.这里的重点是巧妙的防御技术,而不是我们这里70%以上的人已经知道的那些.所以现在是时候深入挖掘你的技巧了.
换句话说,试着想到除了这个无趣的例子之外:
if(5 == x)
而不是 if(x == 5)
:避免意外的分配
以下是一些有趣的最佳防御性编程实践的示例(特定于语言的示例在Java中):
- 锁定变量,直到您知道需要更改它们
也就是说,您可以声明所有变量,final
直到您知道需要更改它为止,此时您可以删除final
.一个常见的未知事实是这对方法参数也有效:
public void foo(final int arg) { /* Stuff Here */ }
- 当发生不好的事情时,留下一丝证据
当你遇到异常时,你可以做很多事情:显然,记录它并执行一些清理会有一些.但是你也可以留下一些证据(比如将变量设置为"UNABLE TO LOAD FILE"等哨兵值,或者99999在调试器中有用,以防你碰巧超过异常catch
块).
- 谈到一致性:魔鬼在细节中
与您正在使用的其他库一致.例如,在Java中,如果要创建一个提取一系列值的方法,则使下限包含且上限为exclusive.这将使其与String.substring(start, end)
以相同方式操作的方法一致.您将在Sun JDK中找到所有这些类型的方法,因为它使各种操作包括元素的迭代与数组一致,其中索引从零(包括)到数组的长度(独占).
那么你最喜欢的防守做法是什么?
更新:如果您还没有,请随意加入.在选择正式答案之前,我有机会获得更多回复.
在c ++中,我曾经喜欢重新定义new,以便它提供一些额外的内存来捕获fence-post错误.
目前,我更倾向于避免采用防御性编程来支持测试驱动开发.如果你快速和外部地发现错误,你就不需要用防御性操作来捣乱你的代码,你的代码是干的,你最终会减少你必须防御的错误.
正如WikiKnowledge所写:
避免防御性编程,快速失败.
防御性编程我指的是编写代码的习惯,这些代码试图弥补数据中的某些失败,编写代码假定调用者可能提供的数据不符合调用者和子例程之间的契约,并且子例程必须以某种方式应对用它.
SQL
当我必须删除数据时,我写了
select * --delete From mytable Where ...
当我运行它时,我会知道我是否忘记或拙劣的where子句.我有安全感.如果一切正常,我会在' - '评论标记之后突出显示所有内容,然后运行它.
编辑:如果我删除了大量数据,我将使用count(*)而不是*
在应用程序启动时分配合理的内存块 - 我认为Steve McConnell将此称为代码完成中的内存降落伞.
这可以用于严重出错的情况并且您需要终止.
预先分配此内存可为您提供安全网,因为您可以将其释放,然后使用可用内存执行以下操作:
保存所有持久数据
关闭所有相应的文件
将错误消息写入日志文件
向用户呈现有意义的错误
在没有默认情况的每个switch语句中,我添加了一个使用错误消息中止程序的情况.
#define INVALID_SWITCH_VALUE 0
switch (x) {
case 1:
// ...
break;
case 2:
// ...
break;
case 3:
// ...
break;
default:
assert(INVALID_SWITCH_VALUE);
}
当你处理枚举的各种状态时(C#):
enum AccountType
{
Savings,
Checking,
MoneyMarket
}
然后,在一些例程中......
switch (accountType)
{
case AccountType.Checking:
// do something
case AccountType.Savings:
// do something else
case AccountType.MoneyMarket:
// do some other thing
default:
--> Debug.Fail("Invalid account type.");
}
在某些时候,我会为此枚举添加另一种帐户类型.当我这样做时,我会忘记修复这个switch语句.所以Debug.Fail
崩溃可怕(在调试模式下)引起我对这个事实的注意.当我添加case AccountType.MyNewAccountType:
,可怕的崩溃停止...直到我添加另一个帐户类型,忘了更新这里的案例.
(是的,多态性在这里可能更好,但这只是我头脑中的一个例子.)
使用字符串(特别是依赖于用户输入的字符串)打印出错误消息时,我总是使用单引号''
.例如:
FILE *fp = fopen(filename, "r");
if(fp == NULL) {
fprintf(stderr, "ERROR: Could not open file %s\n", filename);
return false;
}
这种缺乏引号%s
非常糟糕,因为说文件名是一个空字符串或只是空格或其他东西.打印出来的信息当然是:
ERROR: Could not open file
所以,总是做得更好:
fprintf(stderr, "ERROR: Could not open file '%s'\n", filename);
然后至少用户看到这个:
ERROR: Could not open file ''
我发现这在最终用户提交的错误报告的质量方面产生了巨大的差异.如果有一个像这样看起来很滑稽的错误消息而不是通用的声音,那么他们更有可能复制/粘贴它而不只是写"它不会打开我的文件".
SQL安全
在编写任何将修改数据的SQL之前,我将整个事务包装在回滚事务中:
BEGIN TRANSACTION
-- LOTS OF SCARY SQL HERE LIKE
-- DELETE FROM ORDER INNER JOIN SUBSCRIBER ON ORDER.SUBSCRIBER_ID = SUBSCRIBER.ID
ROLLBACK TRANSACTION
这可以防止您永久执行错误的删除/更新.并且,您可以执行整个过程并验证合理的记录计数或SELECT
在SQL和之间添加语句ROLLBACK TRANSACTION
以确保一切正常.
当你完全确定它你所期望的,改ROLLBACK
到COMMIT
和运行真实的.
适用于所有语言:
将变量范围缩小到最低要求.提供的Eschew变量只是为了将它们带入下一个语句.不存在的变量是您不需要理解的变量,您不能对此负责.出于同样的原因,尽可能使用Lambdas.
如果有疑问,炸弹申请!
检查每个方法开头的每个参数(无论是自己明确地编码,还是使用基于合同的编程在这里都无关紧要),并且如果代码的任何先决条件是正确的异常和/或有意义的错误消息没见过.
当我们编写代码时,我们都知道这些隐含的前置条件,但如果没有明确检查它们,我们会在以后出现问题时为自己创建迷宫,并且数十个方法调用的堆栈将症状的出现和实际位置分开不满足前提条件的地方(=实际存在问题/错误的地方).
在Java中,尤其是使用集合时,请使用API,因此,如果您的方法返回类型List(例如),请尝试以下操作:
public List getList() {
return Collections.unmodifiableList(list);
}
不要让任何事情逃脱你不需要的课程!
在Perl中,每个人都这样做
use warnings;
我喜欢
use warnings FATAL => 'all';
这会导致代码死于任何编译器/运行时警告.这在捕获未初始化的字符串时非常有用.
use warnings FATAL => 'all';
...
my $string = getStringVal(); # something bad happens; returns 'undef'
print $string . "\n"; # code dies here
C#:
string myString = null;
if (myString.Equals("someValue")) // NullReferenceException...
{
}
if ("someValue".Equals(myString)) // Just false...
{
}
在对字符串执行任何操作之前对string.IsNullOrEmpty进行c#检查,如length,indexOf,mid等
public void SomeMethod(string myString) { if(!string.IsNullOrEmpty(myString)) // same as myString != null && myString != string.Empty { // Also implies that myString.Length == 0 //Do something with string } }
[编辑]
现在我也可以在.NET 4.0中执行以下操作,另外检查值是否只是空格
string.IsNullOrWhiteSpace(myString)
在Java和C#中,为每个线程赋予有意义的名称.这包括线程池线程.它使堆栈转储更有意义.甚至为线程池线程提供一个有意义的名称需要花费更多的精力,但如果一个线程池在长时间运行的应用程序中出现问题,我可能会导致堆栈转储(你知道SendSignal.exe,对吗? ),抓住日志,而不必打断正在运行的系统,我可以告诉哪些线程......无论如何.僵局,泄漏,成长,无论问题是什么.
使用VB.NET,默认情况下为整个Visual Studio启用Option Explicit和Option Strict.
使用Java,即使您关闭断言运行生产代码,使用assert关键字也很方便:
private Object someHelperFunction(Object param)
{
assert param != null : "Param must be set by the client";
return blahBlah(param);
}
即使断言断言,至少代码记录了param预期在某处设置的事实.请注意,这是一个私有帮助函数,而不是公共API的成员.此方法只能由您调用,因此可以对如何使用它进行某些假设.对于公共方法,最好为无效输入抛出一个真正的异常.
C++
#define SAFE_DELETE(pPtr) { delete pPtr; pPtr = NULL; } #define SAFE_DELETE_ARRAY(pPtr) { delete [] pPtr; pPtr = NULL }
然后用SAFE_DELETE(pPtr)和SAFE_DELETE_ARRAY(pPtr)替换所有' delete pPtr '和' delete [] pPtr '调用
现在如果你在删除它后使用指针'pPtr',那么你将会出现'访问冲突'错误.它比随机内存损坏更容易修复.
在readonly
找到ReSharper之前我没有找到关键字,但我现在本能地使用它,特别是对于服务类.
readonly var prodSVC = new ProductService();
在Java中,当某些事情发生并且我不知道为什么时,我有时会像这样使用Log4J:
if (some bad condition) { log.error("a bad thing happened", new Exception("Let's see how we got here")); }
通过这种方式,我得到一个堆栈跟踪,向我展示我是如何进入意外情况的,说一个永远不会解锁的锁,一个不能为null的null,依此类推.显然,如果抛出一个真正的异常,我不需要这样做.这时我需要查看生产代码中发生的事情而不会实际干扰其他任何事情.我不希望抛出一个异常,我没赶上之一.我只是希望使用适当的消息记录堆栈跟踪,以便向我标记正在发生的事情.
如果您使用的是Visual C++,请在覆盖基类的方法时使用override关键字.这样,如果有人碰巧更改了基类签名,它将抛出编译器错误,而不是静默调用错误的方法.如果它早先存在,这将节省我几次.
例:
class Foo { virtual void DoSomething(); } class Bar: public Foo { void DoSomething() override { /* do something */ } }
我已经在Java中学到了几乎永远不会无限期地等待锁定解锁,除非我真的希望它可能需要无限期的长时间.如果实际上,锁定应在几秒钟内解锁,那么我将只等待一段时间.如果锁没有解锁,那么我会抱怨并将堆栈转储到日志中,并且根据系统稳定性的最佳状态,继续执行,就像锁解锁一样,或者继续,就像锁从未解锁一样.
在我开始这样做之前,这有助于隔离一些神秘的竞争条件和伪死锁条件.
C#
在公共方法中验证引用类型参数的非空值.
我sealed
为类使用了很多,以避免在我不想要的地方引入依赖项.允许继承应该明确而不是偶然.
当您发出错误消息时,至少尝试提供程序在决定抛出错误时所具有的相同信息.
"权限被拒绝"告诉您存在权限问题,但您不知道问题发生的原因或位置."无法写入事务日志/我的/文件:只读文件系统"至少让你知道做出决定的基础,即使它是错的 - 特别是如果它是错的:错误的文件名?打错了?其他意外错误? - 当你遇到问题时,让你知道你在哪里.
在C#中,使用as
关键字进行强制转换.
string a = (string)obj
如果obj不是字符串,则会抛出异常
string a = obj as string
如果obj不是字符串,则将as留空
您仍然需要考虑null,但这通常更直接,然后寻找强制转换异常.有时您需要"强制转换"类型行为,在这种情况下(string)obj
,首选语法.
在我自己的代码中,我发现我使用as
语法大约75%的时间,(cast)
语法大约25%.
准备好任何输入,并获得任何意外的输入,转储到日志.(在合理范围内.如果您正在读取用户的密码,请不要将其转储到日志中!并且不要每秒将数千种这类消息记录到日志中.在记录之前有关内容,可能性和频率的原因.)
我不只是谈论用户输入验证.例如,如果您正在阅读希望包含XML的HTTP请求,请为其他数据格式做好准备.我很惊讶地看到HTML响应,我只期望XML - 直到我看到并看到我的请求是通过透明代理我不知道并且客户声称无知 - 并且代理超时试图完成请求.因此,代理向我的客户端返回了一个HTML错误页面,混淆了只需要XML数据的客户端.
因此,即使您认为自己控制了电线的两端,也可以获得意想不到的数据格式而不涉及任何恶意.做好准备,防御性编码,并在意外输入的情况下提供诊断输出.
Java的
java api没有不可变对象的概念,这很糟糕!在这种情况下,Final可以帮助你.标签每类是不可变的与最终并准备类相应.
有时在局部变量上使用final是有用的,以确保它们永远不会改变它们的值.我发现这在丑陋但必要的循环结构中很有用.它只是容易意外地重用变量,即使它是一个常量.
在getter中使用防御复制.除非返回基本类型或不可变对象,否则请确保将对象复制为不违反封装.
永远不要使用克隆,使用复制构造函数.
学习equals和hashCode之间的契约.这经常被违反.问题是它在99%的情况下不会影响您的代码.人们覆盖等于,但不关心hashCode.有些实例会使您的代码破坏或行为异常,例如将可变对象用作映射中的键.
我尝试使用契约式设计方法.它可以通过任何语言模拟运行时间.每种语言都支持"断言",但是编写更好的实现可以让您轻松便捷地以更有用的方式管理错误.
在排名前25位最危险的编程错误的"不正确的输入验证"是在一节"组件间不安全互动"最危险的错误.
在方法开头添加前置条件断言是确保参数一致的好方法.在方法结束时,我编写后置条件,检查输出是什么意思.
为了实现不变量,我在任何检查"类一致性"的类中编写一个方法,该类应该由前置条件和后置条件宏自动调用.
我正在评估代码合同库.
我忘了用echo
PHP 写太多次了:
bar->baz(); ?> bar->baz(); ?>
我会永远尝试找出原因 - > baz()没有返回任何东西,而实际上我并没有回应它!:-S所以我创建了一个EchoMe
类,它可以包含任何应该回显的值:
str = strval($value); } function __toString() { $this->printed = true; return $this->str; } function __destruct() { if($this->printed !== true) throw new Exception("String '$this->str' was never printed"); } }
然后在开发环境中,我使用EchoMe来包装应该打印的东西:
function baz() { $value = [...calculations...] if(DEBUG) return EchoMe($value); return $value; }
使用该技术,第一个错过的例子echo
现在会引发异常...