最喜欢的(聪明的)防守编程的最佳实践
如果你不得不select自己喜欢的 (聪明的)防守编码技术,他们会是什么? 虽然我现在的语言是Java和Objective-C(带有C ++背景),但可以随意用任何语言来回答。 这里强调的是除了我们这里70%以上已经知道的那些聪明的防御技术外。 所以现在是时候深入挖掘你的技巧。
换句话说,除了这个无趣的例子之外,试着想一想:
-
if(5 == x)
而不是if(x == 5)
:以避免意外的分配
下面是一些有趣的最佳防御性编程实践的一些例子(特定于语言的例子在Java中):
– locking你的variables,直到你知道你需要改变它们
也就是说,你可以声明所有的variables,直到你知道你将需要改变它,在这一点上,你可以删除final
。 一个普遍未知的事实是,这也适用于方法参数:
public void foo(final int arg) { /* Stuff Here */ }
– 当发生不良事件时,留下一些证据
当你有一个exception时,你可以做很多事情:显然logging它并执行一些清理将是一些。 但是你也可以留下一些证据(例如,设置variables为“无法加载文件”或99999的哨兵值将在debugging器中很有用,以防碰巧发生exceptioncatch
块)。
– 当谈到一致性:魔鬼是在细节
与您正在使用的其他库一致。 例如,在Java中,如果您正在创build一个提取值的范围的方法,则使下限和上限排他 。 这将使它与像String.substring(start, end)
这样的方法一致,它以相同的方式运行。 您将在Sun JDK中find所有这些types的方法,因为它可以执行各种操作,包括迭代与数组一致的元素,其中索引从零( 包含 )到数组长度( 不包括 )。
那么你们最喜欢的防守做法是什么?
更新:如果你还没有,可以随意join。在我select官方答案之前,我有机会提供更多的答案。
在c ++中,我曾经喜欢重新定义新的内容,以便提供一些额外的内存来捕捉fence-post错误。
目前,我更喜欢避免防御性编程,而倾向于testing驱动开发 。 如果你快速和对外地发现错误,你就不需要用防御性的手段来篡改你的代码,你的代码就会干干净净,而且你可以用更less的错误来解决问题。
由于WikiKnowledge写道 :
避免防御性编程,而不是快速失败。
通过防御性编程,我的意思是编写代码的习惯,试图弥补数据中的某些故障,编写假定调用者可能提供的数据不符合调用者和子例程之间的契约的数据,并且子例程必须以某种方式应对用它。
SQL
当我必须删除数据,我写
select * --delete From mytable Where ...
当我运行它的时候,我会知道我是否忘记或者弄糟了where子句。 我有安全感 如果一切正常,我会在“ – ”注释标记之后突出显示所有内容,然后运行它。
编辑:如果我删除了大量的数据,我会用count(*)而不是*
在应用程序启动时分配合理的内存块 – 我认为Steve McConnell在Code Complete中将此称为内存降落伞 。
这可以用于万一严重出错,你需要终止。
预先分配这个内存为您提供了一个安全网,您可以释放它,然后使用可用的内存来执行以下操作:
- 保存所有持久数据
- closures所有适当的文件
- 将错误消息写入日志文件
- 向用户提出有意义的错误
在没有缺省情况的每个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."); }
在某些时候,我会为此枚举添加另一个帐户types。 而当我这样做时,我会忘记修复这个switch语句。 所以Debug.Fail
崩溃(在debugging模式)可怕地吸引我注意这个事实。 当我添加case AccountType.MyNewAccountType:
恐怖的崩溃停止…直到我添加另一个帐户types,忘记更新这里的情况。
(是的,多态在这里可能更好,但这只是我头顶的一个例子。)
当用string(特别是依靠用户input的string)输出错误信息时,我总是使用单引号''
。 例如:
FILE *fp = fopen(filename, "r"); if(fp == NULL) { fprintf(stderr, "ERROR: Could not open file %s\n", filename); return false; }
这个缺less%s
的引号是非常糟糕的,因为filename是一个空string或者只是空格或者其他东西。 打印出来的信息当然是:
ERROR: Could not open file
所以,总是要做的更好:
fprintf(stderr, "ERROR: Could not open file '%s'\n", filename);
那么至less用户看到这个:
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
这可以防止您永久执行错误的删除/更新。 而且,您可以执行整个ROLLBACK TRANSACTION
并validation合理的logging计数,或者在SQL和ROLLBACK TRANSACTION
之间添加SELECT
语句以确保一切正常。
当你完全确定它做到了你所期望的,把ROLLBACK
改为COMMIT
并且真正运行。
对于所有语言:
尽可能减lessvariables的范围 。 回避刚刚提供的variables ,将其带入下一个语句。 不存在的variables是你不需要了解的variables,你不能为此负责。 尽可能使用Lambda,原因相同。
在Java中,特别是集合,使用API,所以如果你的方法返回typesList(例如),请尝试以下操作:
public List<T> getList() { return Collections.unmodifiableList(list); }
不要让任何东西逃离你不需要的课程!
如有疑问,请将应用程序炸毁!
检查每个方法开始时的每个参数(无论是自己明确地编码,还是使用基于合同的编程在这里都不重要),并在代码的任何先决条件是正确的exception和/或有意义的错误消息没见过。
当我们编写代码的时候 ,我们都知道这些隐含的先决条件,但是如果没有明确地检查它们,当以后出现错误时,我们正在为自己创build迷宫,并且几十个方法调用的堆栈将症状的出现与实际位置前提条件没有得到满足(=问题实际在哪里)。
在Perl中,每个人都这样做
use warnings;
我喜欢
use warnings FATAL => 'all';
这会导致代码死于任何编译器/运行时警告。 这在捕获未初始化的string时非常有用。
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... { }
在c#中检查string.IsNullOrEmpty之前,对长度,indexOf,mid等string进行任何操作
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中,默认情况下,Option Explicit和Option Strict会被整个Visual Studio打开。
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)replace所有' delete pPtr '和' delete [] pPtr '
现在如果在删除它后使用指针“pPtr”,就会出错,您将会看到“访问冲突”错误。 修复比随机内存损坏要容易得多。
直到findReSharper,我才findreadonly
关键字,但我现在直接使用它,特别是对于服务类。
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不能为空的东西,等等。 显然,如果抛出一个真正的exception,我不需要这样做。 这是当我需要查看生产代码中发生的事情而又不想干扰别的事情的时候。 我不想抛出一个例外,我没有抓到一个。 我只是想用一个适当的消息logging一个堆栈跟踪来标记我正在发生的事情。
使用Java,使用assert关键字可能会很方便,即使您运行断言closures的生产代码:
private Object someHelperFunction(Object param) { assert param != null : "Param must be set by the client"; return blahBlah(param); }
即使断言,至less代码文件param预计将设置在某个地方的事实。 请注意,这是一个私人助手function,而不是公共API的成员。 这个方法只能由你来调用,所以可以对如何使用这个方法做一些假设。 对于公共方法来说,为无效input引发一个真正的exception可能会更好。
C#
- 在公共方法中validation参考types参数的非空值。
- 为了避免在不需要的地方引入依赖关系,我使用了很多
sealed
类。 允许inheritance应该明确而不是偶然地进行。
如果您使用的是Visual C ++,则只要覆盖基类的方法,就可以使用override关键字 。 这样,如果任何人碰巧改变基类签名,它将抛出一个编译器错误,而不是默默地调用错误的方法。 如果早一点存在的话,这可能会救我几次。
例:
class Foo { virtual void DoSomething(); } class Bar: public Foo { void DoSomething() override { /* do something */ } }
我从Java中学到几乎永远不会无限期地等待一个锁解锁,除非我真的期望它可能需要无限期的时间。 如果现实的话,锁应该在几秒钟内解锁,那么我只会等待一段时间。 如果锁没有解锁,那么我会抱怨并将堆栈转储到日志中,并根据最适合系统稳定性的情况继续打开锁,或者像锁永不解锁一样继续。
这样做有助于在开始这样做之前,隔离一些神秘的竞争条件和伪死锁条件。
在C#中,使用as
关键字进行强制转换。
string a = (string)obj
如果obj不是string,会抛出一个exception
string a = obj as string
如果obj不是string,会留下一个空值
您仍然需要考虑空值,但是通常这是比较直接的,然后查找转换exception。 有时候你想要“铸造或炸毁”types的行为,在这种情况下(string)obj
语法是首选。
在我自己的代码中,我发现大约75%的时间使用as
语法,大约25%使用(cast)
语法。
当你发出错误信息时,至less要提供程序在做出错误决定时提供的相同信息。
“权限被拒绝”告诉你有一个权限问题,但你不知道问题发生的原因或地点。 “无法写入事务日志/我的/文件:只读文件系统”至less让你知道做出决定的依据,即使它是错的 – 特别是如果它是错误的:错误的文件名? 开错了? 其他意外的错误? – 让你知道你在什么时候遇到问题。
准备好任何input,以及任何意外的input,转储到日志。 (Within reason. If you're reading passwords from the user, don't dump that to logs! And don't log thousands of these sorts of messages to logs per second. Reason about the content and likelihood and frequency before you log it.)
I'm not just talking about user input validation. For example, if you are reading HTTP requests that you expect to contain XML, be prepared for other data formats. I was surprised to see HTML responses where I expected only XML — until I looked and saw that my request was going through a transparent proxy I was unaware of and that the customer claimed ignorance of — and the proxy timed out trying to complete the request. Thus the proxy returned an HTML error page to my client, confusing the heck out of the client that expected only XML data.
Thus, even when you think you control both ends of the wire, you can get unexpected data formats without any villainy being involved. Be prepared, code defensively, and provide diagnostic output in the case of unexpected input.
I try to use Design by Contract approach. It can be emulated run time by any language. Every language supports "assert", but it's easy and covenient to write a better implementation that let you manage the error in a more useful way.
In the Top 25 Most Dangerous Programming Errors the "Improper Input Validation" is the most dangerous mistake in the "Insecure Interaction Between Components" section.
Adding precondition asserts at the beginning of the methods is a good way to be sure that parameters are consistent. At the end of methods i write postconditions , that check that output is what's inteded to be.
In order to implement invariants , I write a method in any class that checks "class consistence", that should be called authomatically by precondition and postcondition macro.
I'm evaluating the Code Contract Library .
Java的
The java api has no concept of immutable objects, which is bad! Final can help you in that case. Tag every class that is immutable with final and prepare the class accordingly .
Sometimes it is useful to use final on local variables to make sure they never change their value. I found this useful in ugly, but necessary loop constructs. Its just to easy to accidently reuse a variable even though it is mend to be a constant.
Use defense copying in your getters. Unless you return a primitive type or a immutable object make sure you copy the object to not violate encapsulation.
Never use clone, use a copy constructor .
Learn the contract between equals and hashCode. This is violated so often. The problem is it doesn't affect your code in 99% of the cases. People overwrite equals, but don't care about hashCode. There are instances in wich your code can break or behaves strange, eg use mutable objects as keys in a map.
I forgot to write echo
in PHP one too many times:
<td><?php $foo->bar->baz(); ?></td> <!-- should have been --> <td><?php echo $foo->bar->baz(); ?></td>
It would take me forever to try and figure out why ->baz() wasn't returning anything when in fact I just wasn't echoing it! :-S So I made an EchoMe
class which could be wrapped around any value that should be echoed:
<?php class EchoMe { private $str; private $printed = false; function __construct($value) { $this->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"); } }
And then for the development environment, I used an EchoMe to wrap things which should be printed:
function baz() { $value = [...calculations...] if(DEBUG) return EchoMe($value); return $value; }
Using that technique, the first example missing the echo
would now throw an exception …
when getting a table from a dataset
if( ds != null && ds.tables != null && dt.tables.Count > 0 && ds.tables[0] != null && ds.tables[0].Rows > 0 ) { //use the row; }
C ++
When I type new, I must immediately type delete. Especially for arrays.
C#
Check for null before accessing properties, especially when using the Mediator pattern. Objects get passed (and then should be cast using as, as has already been noted), and then check against null. Even if you think it will not be null, check anyway. I've been surprised.