关于语言无关:最喜欢(聪明)防御性编程最佳实践

Favorite (Clever) Defensive Programming Best Practices

如果你不得不选择你最喜欢的(聪明的)防御编码技术,它们会是什么?虽然我现在的语言是Java和Objtovi-C(具有C++背景),但在任何语言中都可以自由回答。这里强调的是聪明的防守技术,而不是我们70%以上的人已经知道的技术。所以现在是时候深入研究你的技巧了。

换言之,试着想想这个无趣的例子:

  • if(5 == x)代替if(x == 5):避免非预期的分配

下面是一些有趣的最佳防御编程实践的例子(语言特定的例子在Java中):

-锁定变量,直到您知道需要更改它们

也就是说,您可以声明所有变量final,直到您知道需要更改它为止,此时您可以删除final。一个常见的未知事实是,这对方法参数也有效:

1
public void foo(final int arg) { /* Stuff Here */ }

-当一些不好的事情发生时,留下证据的痕迹

当您遇到异常时,您可以做很多事情:显然,记录它并执行一些清理是少数。但是,您也可以留下证据的痕迹(例如,将变量设置为sentinel值(如"无法加载文件"或99999)在调试器中很有用,以防您碰巧遇到异常catch块)。

-说到一致性:细节就是魔鬼。

与正在使用的其他库保持一致。例如,在Java中,如果您正在创建一种提取值范围的方法,则使下限包含和上界独占。这将使其与以相同方式运行的String.substring(start, end)等方法保持一致。在Sun JDK中,您会发现所有这些类型的方法都是这样的,因为它可以进行各种操作,包括与数组一致的元素迭代,其中索引从零(包含)到数组的长度(不包含)。

那么你最喜欢的防守方式是什么?

更新:如果你还没有,请随时插话。在我选择正式答案之前,我有机会得到更多的答复。


在C++中,我曾经喜欢重新定义新的,以便它提供一些额外的内存来捕获栅栏后错误。

目前,我更喜欢避免防御性编程,而喜欢测试驱动开发。如果你能迅速地从外部捕获错误,你就不需要用防御策略来搅乱你的代码,你的代码更加枯燥,最后你能避免的错误更少。

正如维基知识所写:

Avoid Defensive Programming, Fail Fast Instead.

By defensive programming I
mean the habit of writing code that
attempts to compensate for some
failure in the data, of writing code
that assumes that callers might
provide data that doesn't conform to
the contract between caller and
subroutine and that the subroutine
must somehow cope with it.


SQL

当我必须删除数据时,我会写

1
2
3
4
select *    
--delete    
From mytable    
Where ...

当我运行它时,我会知道我是否忘记了或弄错了WHERE子句。我有安全感。如果一切正常,我将突出显示"--"注释标记后的所有内容,然后运行它。

编辑:如果我要删除大量数据,我将使用count(*)而不是*


当应用程序启动时,分配一个合理的内存块——我认为SteveMcConnell称之为代码完成中的内存降落伞。

这可以用于发生严重问题并要求您终止的情况。

预先分配这个内存为您提供了一个安全网,因为您可以释放它,然后使用可用内存执行以下操作:

  • 保存所有持久数据
  • 关闭所有合适的文件
  • 将错误消息写入日志文件
  • 向用户呈现有意义的错误


在每个没有默认情况的switch语句中,我添加了一个使用错误消息中止程序的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define INVALID_SWITCH_VALUE 0

switch (x) {
case 1:
  // ...
  break;
case 2:
  // ...
  break;
case 3:
  // ...
  break;
default:
  assert(INVALID_SWITCH_VALUE);
}


当处理枚举的各种状态时(c):

1
2
3
4
5
6
enum AccountType
{
    Savings,
    Checking,
    MoneyMarket
}

然后,在一些例行程序中…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:时,可怕的崩溃停止了……直到我添加了另一个账户类型,忘记在这里更新案例。

(是的,多态性在这里可能更好,但这只是我头脑中的一个例子。)


当用字符串(特别是依赖于用户输入的字符串)打印错误消息时,我总是使用单引号''。例如:

1
2
3
4
5
6
FILE *fp = fopen(filename,"r");
if(fp == NULL) {
    fprintf(stderr,"ERROR: Could not open file %s
", filename);
    return false;
}

%s周围缺少引号真的很糟糕,因为比如说文件名是空字符串,或者只是空格之类的。打印出来的信息当然是:

1
ERROR: Could not open file

所以,最好这样做:

1
2
fprintf(stderr,"ERROR: Could not open file '%s'
", filename);

然后至少用户会看到:

1
ERROR: Could not open file ''

我发现这对最终用户提交的bug报告的质量有很大的影响。如果有这样一条看起来很有趣的错误消息,而不是一般的声音,那么他们更可能复制/粘贴它,而不是只写"它不会打开我的文件"。


SQL安全

在编写任何将修改数据的SQL之前,我将整个事务打包在一个回滚事务中:

1
2
3
4
BEGIN TRANSACTION
-- LOTS OF SCARY SQL HERE LIKE
-- DELETE FROM ORDER INNER JOIN SUBSCRIBER ON ORDER.SUBSCRIBER_ID = SUBSCRIBER.ID
ROLLBACK TRANSACTION

这可以防止您永久执行错误的删除/更新。而且,您可以执行整个过程并验证合理的记录计数,或者在SQL和ROLLBACK TRANSACTION之间添加SELECT语句,以确保一切正常。

当你完全确信它能达到你的预期时,把ROLLBACK改为COMMIT,然后真正跑起来。


对于所有语言:

把变量的范围缩小到最少。避开刚刚提供的变量,以便将它们携带到下一个语句中。不存在的变量是你不需要理解的变量,你不必为此负责。出于同样的原因,尽可能使用lambda。


如果有疑问,请轰炸应用程序!

在每个方法的开头检查每个参数(无论是自己明确编码,还是使用基于契约的编程在这里都不重要),如果不满足代码的任何前提条件,则使用正确的异常和/或有意义的错误消息进行轰炸。

当我们编写代码时,我们都知道这些隐含的前提条件,但是如果没有明确地检查它们,当稍后发生错误时,我们会为自己创建迷宫,并且成堆的方法调用将症状的发生与不满足前提条件的实际位置分隔开(=问题/错误实际上在哪里)。)


在爪哇,尤其是在集合中,使用API,因此,如果方法返回类型列表(例如),请尝试以下内容:

1
2
3
public List<T> getList() {
    return Collections.unmodifiableList(list);
}

不要让任何你不需要的东西离开你的班级!


C:

1
2
3
4
5
6
7
8
9
10
11
string myString = null;

if (myString.Equals("someValue")) // NullReferenceException...
{

}

if ("someValue".Equals(myString)) // Just false...
{

}


在Perl中,每个人都这样做

1
use warnings;

我喜欢

1
use warnings FATAL => 'all';

这会导致代码因任何编译器/运行时警告而死亡。这在捕获未初始化的字符串时非常有用。

1
2
3
4
5
use warnings FATAL => 'all';
...
my $string = getStringVal(); # something bad happens;  returns 'undef'
print $string ."
";        # code dies here


在对字符串执行任何操作(如长度、indexof、mid等)之前,在C检查string.isNullOrEmpty中

1
2
3
4
5
6
7
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
   }
}

[编辑] < BR/>现在我还可以在.NET 4.0中执行以下操作,这将另外检查值是否只是空白

1
string.IsNullOrWhiteSpace(myString)


在爪哇和C中,给每个线程一个有意义的名字。这包括线程池线程。它使堆栈转储更有意义。甚至给线程池线程赋予一个有意义的名称也需要付出更多的努力,但是如果一个线程池在长时间运行的应用程序中出现问题,我可以导致发生堆栈转储(您确实知道sendsignal.exe,对吧?),获取日志,不必中断正在运行的系统,我就可以知道哪些线程是…无论什么。无论问题是什么,死锁、泄漏、增长。


在vb.net中,默认情况下会为整个Visual Studio打开Option Explicit和Option Strict。


C++

1
2
#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"后错误地使用它,您将得到"访问冲突"错误。它比随机存储器损坏更容易修复。


使用Java,即使断言断言生产代码,也可以很方便地使用AsScript关键字:

1
2
3
4
5
6
private Object someHelperFunction(Object param)
{
    assert param != null :"Param must be set by the client";

    return blahBlah(param);
}

即使断开断言,至少代码记录了这样一个事实,即参数应该设置在某个地方。请注意,这是一个私有助手函数,而不是公共API的成员。此方法只能由您调用,因此可以对如何使用它进行某些假设。对于公共方法,最好对无效输入抛出一个真正的异常。


在Java中,当一些事情发生,我不知道为什么,我有时会使用Log4J这样:

1
2
3
if (some bad condition) {
    log.error("a bad thing happened", new Exception("Let's see how we got here"));
}

这样我就得到了一个堆栈跟踪,显示我是如何进入意外情况的,比如说一个从未解锁的锁,一个不能为空的空值,等等。显然,如果抛出一个真正的异常,我不需要这样做。这是我需要看到生产代码中发生的事情,而实际上不干扰任何其他事情的时候。我不想抛出一个异常,我也没有抓住一个。我只想记录一个带有适当消息的堆栈跟踪,以将我标记为正在发生的事情。


直到找到resharper,我才找到readonly关键字,但现在我本能地使用它,特别是在服务类中。

1
readonly var prodSVC = new ProductService();


如果您使用VisualC++,那么在您重载基类的方法时,请使用覆盖关键字。这样,如果有人碰巧更改了基类签名,它将抛出一个编译器错误,而不是静默调用错误的方法。如果它早一点就存在的话,这会救我几次。

例子:

1
2
3
4
5
6
7
8
9
class Foo
{
   virtual void DoSomething();
}

class Bar: public Foo
{
   void DoSomething() override { /* do something */ }
}


C.*

  • 验证公共方法中引用类型参数的非空值。
  • 我经常为类使用sealed,以避免在我不需要的地方引入依赖项。允许继承应该是明确的,而不是偶然的。

当您发出错误消息时,至少要尝试提供程序在决定抛出错误时所拥有的相同信息。

"权限被拒绝"告诉您存在权限问题,但您不知道问题发生的原因或位置。无法写入事务日志/my/file:read-only filesystem"至少可以让您知道做出决定的依据,即使是错误的——尤其是错误的:错误的文件名?开错了?其他意外错误?-让你知道当你遇到问题的时候你在哪里。


我在Java中学到了几乎永远不会等待锁的解锁,除非我真的认为它可能需要无限长的时间。如果现实地说,锁应该在几秒钟内解锁,那么我只会等一段时间。如果锁没有解锁,那么我会抱怨并将堆栈转储到日志中,根据对系统稳定性最好的方式,要么像锁解锁一样继续,要么像锁从未解锁一样继续。

这有助于隔离一些竞争条件和伪死锁条件,这些条件在我开始这样做之前是神秘的。


在c_中,使用as关键字进行强制转换。

1
string a = (string)obj

如果obj不是字符串,将引发异常

1
string a = obj as string

如果obj不是字符串,则将保留为空

您仍然需要考虑空值,但这通常比寻找强制转换异常更直接。有时您需要"强制转换或放大"类型行为,在这种情况下,最好使用(string)obj语法。

在我自己的代码中,我发现我使用as语法的时间约为75%,使用(cast)语法的时间约为25%。


爪哇

Java API没有不可变对象的概念,这是不好的!在这种情况下,期末考试可以帮助你。用final标记每个不可变的类,并相应地准备该类。

有时,对局部变量使用final可以确保它们永远不会改变其值。我发现这在丑陋但必要的循环结构中很有用。即使修改为常量,也很容易意外地重用变量。

在getter中使用防御复制。除非返回基元类型或不可变对象,否则请确保复制该对象不会违反封装。

不要使用克隆,请使用复制构造函数。

学习equals和hashcode之间的契约。这是经常被侵犯的。问题是,在99%的情况下,它不会影响您的代码。人们重写equals,但不关心hashcode。在wich中有一些实例,您的代码可能会中断或行为怪异,例如使用可变对象作为地图中的键。


准备好任何输入,以及任何意外的输入,转储到日志中。(在合理范围内。如果您正在从用户那里读取密码,不要将其转储到日志中!不要每秒记录上千条这样的消息。在记录内容、可能性和频率之前的原因。)

我不仅仅是在谈论用户输入验证。例如,如果您正在读取希望包含XML的HTTP请求,请为其他数据格式做好准备。我很惊讶地看到HTML响应,我只期望XML——直到我看到我的请求正在通过一个我不知道的透明代理,并且客户声称不知道——代理在试图完成请求时超时。因此,代理向我的客户机返回了一个HTML错误页,从而混淆了客户机中只需要XML数据的检查。

因此,即使您认为自己控制了连接的两端,您也可以获得意想不到的数据格式,而不涉及任何恶行。做好准备,进行防御性编码,并在出现意外输入时提供诊断输出。


我尝试使用按合同设计的方法。它可以被任何语言模拟运行时。每种语言都支持"断言",但编写一个更好的实现,使您能够以更有用的方式管理错误,这很容易,也很重要。

在前25个最危险的编程错误中,"不正确的输入验证"是"组件间不安全交互"部分中最危险的错误。

在方法的开头添加前置条件断言是确保参数一致的好方法。在方法的最后,我编写后置条件,检查输出是否是被集成的。

为了实现不变量,我在任何检查"类一致性"的类中编写了一个方法,该方法应该通过前置条件和后置条件宏来自动调用。

我正在评估代码合同库。


我忘记用php编写echo太多次了:

1
2
3
<td><?php $foo->bar->baz(); ?></td>
<!-- should have been -->
<td><?php echo $foo->bar->baz(); ?></td>

我要花很长时间才能弄明白为什么->baz()没有返回任何东西,而实际上我只是没有响应它!:-s所以我做了一个EchoMe类,它可以被包装在任何应该回送的值上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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");
  }
}

然后对于开发环境,我使用Echome来包装应该打印的内容:

1
2
3
4
5
6
function baz() {
  $value = [...calculations...]
  if(DEBUG)
    return EchoMe($value);
  return $value;
}

使用这种技术,第一个缺少echo的例子现在会抛出一个异常…


从数据集中获取表时

1
2
3
4
5
6
7
8
9
if(  ds != null &&
     ds.tables != null &&
     dt.tables.Count > 0 &&
     ds.tables[0] != null &&
     ds.tables[0].Rows > 0 )
{

    //use the row;
}


使用允许动态、运行时日志级别调整的日志记录系统。通常,如果必须停止程序以启用日志记录,则会丢失发生错误的任何罕见状态。您需要能够在不停止进程的情况下打开更多的日志记录信息。

另外,Linux上的"strace-p[pid]"将显示您希望系统调用正在进行的进程(或Linux线程)。一开始可能看起来很奇怪,但是一旦你习惯了libc所调用的系统调用,你就会发现这对于现场诊断是无价的。


始终在最高警告级别编译,并将警告视为错误(构建断路器)。

即使代码是"正确的",也要在不禁用警告的情况下修复警告原因(如果可能的话)。例如,您的C++编译器可能会给您这样的法律代码警告:

1
while (ch = GetNextChar()) { ... }

看起来您可能输入了=,而不是==。如果添加显式检查,大多数提供此(有用)警告的编译器都将关闭。

1
while ((ch = GetNextChar()) != 0) { ... }

稍微明确一点,不仅可以消除警告,而且可以帮助下一个必须理解代码的程序员。

如果必须禁用警告,请在代码中使用#pragma,这样可以(1)限制禁用警告的代码范围;(2)使用注释解释为什么必须禁用警告。命令行或生成文件中禁用的警告是等待发生的灾难。


使用一些基于接口的OOP模式的sentinel类,而不是null

例如,当使用

1
2
3
public interface IFileReader {
  List<Record> Read(string file);
}

使用哨兵类

1
2
3
4
5
6
7
8
9
10
public class NoReader : IFileReader {
  List<Record> Read(string file) {
    // Depending on your functional requirements in this case
    // you will use one or more of any of the following:
    // - log to your debug window, and/or
    // - throw meaningful exception, and/or
    return new List<Record>(); // - graceful fall back, and/or
    // - whatever makes sense to you here...
  }
}

并使用它初始化任何ifilereader变量

1
IFileReader reader = new NoReader();

而不是仅仅把它们留给null(含蓄地或明确地)

1
2
IFileReader reader; /* or */
IFileReader reader = null;

以确保不会出现意外的空指针异常。

额外的好处:你不必再将每个IFileReader变量与if (var!=null) ...一起使用了,因为它们不会是null


C++

当我键入new时,必须立即键入delete。尤其是阵列。

C.*

在访问属性之前检查是否为空,特别是在使用中介模式时。对象被传递(然后应该使用as强制转换,正如已经指出的那样),然后检查是否为空。即使您认为它不会为空,也要检查。我很惊讶。


C++:自动检测数组大小

1
char* mystrings[] = {"abc","xyz" ,"pqr" }

通常情况下,for写得像

1
2
3
4
5
for (int i=0; i< 3; i++)
{
    str= mystrings[i]
    // somecode
}

不过,稍后您可以向"mystrings"添加更多新字符串。在这种情况下,上面的for循环可能会在代码中引入一些细微的错误。

我使用的解决方案是

1
2
3
4
5
6
int mystringsize = sizeof(mystrings)/sizeof(char*)
for (int i=0; i< mystringsize; i++)
{
    str= mystrings[i]
    // somecode
}

现在,如果您向"mystrings"数组添加更多的字符串,for循环将自动调整。


在Python中,如果我截取(或更改一个方法),但那天没有时间测试它,那么我就塞进一个"assert-false"中,这样,如果该方法运行,代码就会崩溃,从而产生令人尴尬的错误,第二天我会注意到。有意的语法错误也会有帮助。

例子:

1
2
3
4
def somefunction(*args,**kwargs):
    ''' <description of function and args> '''
    # finish this in the morning
    assert False,"Gregg finish this up"


记住,例外是程序员最好的朋友——永远不要吃它们!


  • 微小的可理解类。他们中的很多人。
  • 微小的可理解的方法。
  • 尽可能不变。
  • 最小化范围——没有可以打包的公共包,没有可以私有的包。
  • 永远不要为公共可变变量找借口。

而且,当你的课程规模很小,而且通常都是最后一门课时,防守是很便宜的——不管你是否相信,也可以把它扔进去。测试正在传递给构造函数和(如果确实必须有)setter的值。


我的C++指南,但我不认为这是聪明的:

  • 总是把它当作makefile的一部分。更好的是,尽可能使用隐蔽性。
  • 不要使用C++异常。
  • 不要把太多的东西放在C++构造函数上。改用init()方法。唯一在构造函数中发出错误信号的方法是异常,即PITA。
  • 除非有必要,否则不要超载操作员。
  • 如果构造函数有一个参数,请始终使用显式关键字。
  • 避免全局对象。他们的执行令不能保证。
  • 在类分配内存时定义复制构造函数。但是,如果您不希望复制类,而且您太懒了,无法定义类,那么就要防止它被调用。
1
2
3
4
5
class NonCopied {
private:
    NonCopied(const NonCopied&);
    NonCopied& operator=(const NonCopied&);
}
  • 停止使用sprintf()、strcpy()、strcat()。使用它们的替代品,例如snprintf、strncpy()等。


使用一个控制台,就像在游戏中一样;

不是完全的"防守",但我从很多比赛中看到的。

我喜欢有一个完整的控制台,用于我的所有应用程序,它允许我:

  • 定义要从控制台调用的简单命令(如切换到调试模式、设置一些运行时变量、检查内部配置参数等)。
  • 在应用程序运行时,随时从应用程序访问日志。
  • 如果需要,将日志保存到文件
  • 将每个未处理的异常记录到控制台,然后再将其提交给用户(如果可以)。这样,每一个异常都会被作为某种级别捕获。如果您巧妙地将其与调试信息或映射文件结合在一起,就可以获得出色的结果。
  • 在C中,如果用Conditional属性标记控制台方法,那么它们将自动从发布版本中剥离。在其他语言中,可以通过预处理器指令实现相同的功能。

    我发现它在测试阶段特别有价值,因为它允许开发人员查看正在发生的事情,并且测试人员可以向开发人员提供更好的反馈。

    此外:

    • 永远不要只为罗金而破例。
    • 从不捕获一般异常(异常E)
    • 从不隐藏例外
    • 将编译器警告视为错误,只接受经过仔细研究的警告。
    • 始终检查来自库外的每个输入。
    • 在"调试"中检查来自库中的输入,不要签入发行版。
    • 从不引发一般异常。如果存在描述问题的异常,请使用它,否则,请创建自己的异常。

    在C++ EDCOX1中,0是一个非常方便的工具。我不仅提供了评估的条件,而且还提供了一条说明问题所在的信息:

    1
    assert( isConditionValid &&"ABC might have failed because XYZ is wrong." );

    如果没有要检查的实际变量,或者您发现自己处于不应该发生的情况下("default"handler of switch()),这也会起作用:

    1
    assert( 0 &&"Invalid parameter" );

    它不仅在调试模式下断言,而且还告诉您同时发生了什么错误。

    如果我没记错的话,我从"C++编码标准"中得到这个。


    在Perl中,当子例程没有传递足够的参数时die()。这可以防止您获得必须通过堆栈向上跟踪10个级别的失败。

    1
    2
    3
    4
    5
    6
    sub foo {
        my $param0 = shift or confess"param0 is a required param";
        my $param1 = shift or confess"param1 is a required param";
        my $param2 = shift or confess"param2 is a required param";
        ...
    }


    不要把赤裸裸的收藏品,甚至是仿制品传出去。它们不能被保护,也不能有附加的逻辑。

    一个好的并行方法是使用公共变量而不是setter/getter。setter/getter允许您在不影响外部世界的情况下更改底层实现。

    如果要传递一个集合,如何在不影响外部世界的情况下更改数据结构?集合的所有访问权限都分布在所有代码中!!

    相反,把它包装起来,给自己一个地方放一点业务逻辑。完成后,您将发现一些不错的重构。

    通常,您会发现添加一些变量和第二个集合是有意义的——然后您会发现这个类一直在丢失!


    当做多线程C/C++编程时,创建一系列宏,声明你的函数正在被调用的线程上调用。然后充分利用它们。

    • 在线程上断言
    • 在工作线程上断言
    • 在线程池线程上断言
    • 在下载线程上断言
    • 等。

    初始化线程时,在Windows上使用getcurrenthreaddid()或在posix上使用pthread_self(),然后存储在全局中。断言与存储值进行比较。

    已经为我节省了许多痛苦的调试,尤其是当其他人重构现有的多线程代码时。


    几周内尽量不要做任何你设计的东西。通常情况下,在事情被锁定之前,其他情况就会出现。


    • 使代码尽可能可读,尤其是使用尽可能明显的函数和变量名。如果这意味着有些名字有点长,那就顺其自然吧。

    • 尽可能使用静态分析仪。你很快就养成了写符合规则的代码的习惯。

    • 在开发过程中,可以很容易地打开诊断输出,但也可以很容易地将其关闭用于生产。


    回到那些RAM没有空闲的日子,大多数计算机都非常有限,"内存不足!"是一个很常见的错误信息…

    好吧,大多数应用程序都能"优雅"地崩溃:用户(几乎)从未丢失过他们的作品。

    (几乎,我说了!^ ^)。

    怎么做到的?非常简单:当你的应用程序启动时,分配一个气球状的RAM(比如,高达20KB!)。然后,当调用malloc()失败时:

  • 请说"内存不足"(此消息是强制性的)。
  • 加上"你最好把所有的工作都保存下来。现在!"
  • 释放20千字节的大气球。
  • 简历。
  • ET Voice。你的应用程序在用户面前崩溃得很慢,大多数情况下,可以节省它的工作。


    设计您的日志策略,以便在生产中发生错误时,会自动向适当的支持人员或开发人员发送电子邮件。这允许您主动发现错误,而不是等待用户抱怨。

    请注意,这样做应该谨慎。我举了一个例子,一个开发人员在一个循环中编写了一些日志代码。几个月后,系统中的一个错误触发了此代码。不幸的是,应用程序位于该循环中,一次又一次地记录相同的错误。那天早上我们到达办公室,得知我们的日志框架在凌晨4点到8点之间发送了40000封电子邮件后,我们的邮件服务器崩溃了!


    在C++中

    我将断言分散到我的函数中,特别是在函数的开始和结束时,以捕获任何意外的输入/输出。当我稍后在函数中添加更多功能时,断言将帮助我记住。它还帮助其他人了解函数的意图,并且仅在调试模式下处于活动状态。

    我尽量避免使用指针,而是使用引用,这样我就不需要在代码中放置混乱的if (NULL!=p)语句。

    我也尽可能频繁地在声明和函数/方法参数中使用const这个词。

    我也避免使用豆荚,而是尽可能多地使用stl/boost,以避免MEM泄漏和其他令人讨厌的事情。但是,我确实避免使用过多的自定义模板,因为我发现它们很难调试,特别是对于那些没有编写代码的人。


    如果有一个值类型对它的值有某些约束,那么就创建一个类,在该类中这些约束由代码强制执行。一些例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public class SanitizedHtmlString
    {
    private string val;

    public SanitizedHtmlString(string val)
    {
      this.val = Sanitize(val);
    }

    public string Val
    {
      get { return val; }
    }

    //TODO write Sanitize method...
    }


    public class CarSpeed
    {
    private int speedInMilesPerHour;

    public CarSpeed(int speedInMilesPerHour)
    {
      if (speedInMilesPerHour > 1000 || speedInMilesPerHour < 0)
      {
        throw new ArgumentException("Invalid speed.");
      }
      this.speedInMilesPerHour = speedInMilesPerHour;
    }

    public int SpeedInMilesPerHour
    {
      get { return speedInMilesPerHour; }
    }
    }


    那么,在编写递归方法时,我们中谁没有意外地将Visual Studio锁定一段时间呢?

    1
    2
    3
    4
    5
    6
    7
      int DoSomething(int value)  // stupid example for illustrative purposes
      {
          if (value < 0)
              return value;
          value++;  // oops
          return DoSomething(value);
      }

    为了避免不得不等待的烦恼,有时还可能需要使用任务管理器来终止IDE,请在调试递归方法时将其包含在递归方法中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
      int DoSomething(int value)
      {
    >>    if (new StackTrace().FrameCount > 1000)  // some appropriately large number
    >>        Debug.Fail("Stack overflow headed your way.");

          if (value < 0)
              // some buggy code that never fires
          return DoSomething(value);
      }

    这看起来可能很慢,但实际上检查帧计数非常快(在我的电脑上不到一秒钟)。在您确定这个方法工作正常之后,您可以去掉这个故障保护(或者只是注释掉它,但留给以后的调试)。


    语言不可知论:不要依赖编译器、虚拟机等初始化。始终将变量显式初始化为有用的值。

    断言是您最好的朋友,尽管单元测试在某些情况下更适合。

    C/C++:尽可能使用基于堆栈的对象。

    在条件中,显式检查期望值或不期望值。例如,如果您有一个名为activated的布尔变量,而不是写入if (activated),则写入if (true == activated)。原因是activated可能包含足够好的垃圾,从而使条件成功。


    简而言之,只崩溃的软件不需要一些关闭过程和一些自己使用的(可能是错误的)恢复代码,而是总是通过"崩溃"来停止程序,因此在启动时总是运行恢复代码。

    这并不适用于所有事情,但在某些情况下,这是一个非常好的想法。


    我在PHP中做的一些事情(错误很容易,而且常常是灾难性的):

    • 打开vim中突出显示提示的所有语法。默认情况下有很多关闭(:help php可以看到它们)。我正在考虑添加一些突出显示我自己的东西的错误…
    • 使用git中的预提交挂钩对每个更改的文件进行语法检查(php -l)。它只防止基本错误进入,但总比什么都没有好。
    • 在数据库类周围编写包装纸,使参数化的准备好的语句比键入普通查询更容易死掉——$db->q1($sql, $param1, $param2)获取第一行的单列,等等。
    • 配置它(通过xdebug扩展)来为甚至是微不足道的警告消息吐出巨大的调试信息HTML表,因此不可能忽略它们。在dev服务器上。在生产中,它们会被静默地记录下来。
    • 使事情简短、简单和明显。我花了很多时间只是为了制作更小的文件而重构一些东西。
    • 使用显式控制结构语法,以避免几个""靠近。
    • 在代码签入前对其进行校对。我有一个习惯,就是把窗口最大化,然后设置一个荒谬的大字体。如果我只能在屏幕上以很小的字体同时看到132C x 50R,那就太长了。


    If (some really bad condition) Then
    Throw New Exception("particular bad thing happened")
    End If

    通常是这样的

    公共子新建(键为guid)dim oreturn as returnpacket=services.tablebackedObjectServices.getData(key)如果oreturn.ds.tables(0).rows.count=0,则引发新异常("在数据库中找不到从键加载的TableBackedObject。")结束如果

    因为只有在从搜索过程的结果中选择某个特定的对象后加载该对象时,才应该调用该特定的构造函数,因此找不到该对象是bug或争用条件(这意味着另一个用户会按键删除该对象)。


    JavaScript:

    我们应该适当地使用"=="和"=="。

    == : type-converting equality comparison

    === : strict equality comparison

    例如,"1"==1为真,但"1"==1为假。

    很多人不自觉地用"=="代替"=="。


    在我的工作中,我做了很多数学测试,测试来自Teradyne(C,VBA),AdvestMeST(C++.NET)的自动测试设备上的混合信号半导体,等等。

    我使用的两种防御策略是:

    • 防止被零除,如果(X)!= 0){Z= Y/X;}否则{/*给Z一个可识别的假号码,继续程序*/}

    • 不要将零或负数传递给日志计算。这对于增益、CMRR和PSRR的计算很常见。如果(x>0){psrr=20*log(x);}否则{psrr=-999;/*假号码*/}

    有些人可能反对使用假数字,但这些程序被用于非常大的半导体制造业。如果在测试坏部件时发生错误,最好继续测试并保持数据格式的完整性。在测试数据的后处理过程中,伪数字很容易作为离群值分开。

    ——迈克


    C++:

    避免使用原始指针,始终使用Boost智能指针包(例如共享指针)。


    循环索引不使用单字符变量。例如:

    1
    2
    for (int ii = 0 ; ii < someValue ; ii++)
        // loop body

    这是一个简单的习惯,但是如果必须使用标准的文本编辑器来查找对循环变量的引用,这是非常有用的。当然,索引循环通常不应该太长,以至于需要搜索索引引用…


    包括高级异常处理,如这里详细描述的那样

    Windows窗体应用程序中的顶级异常处理

    我的program.cs会像这样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
        static class Program
        {
        [STAThread]
        static void Main()
        {
            Application.ThreadException +=
                new ThreadExceptionEventHandler(new ThreadExceptionHandler().ApplicationThreadException);

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());
        }

        public class ThreadExceptionHandler
        {
            public void ApplicationThreadException(object sender, ThreadExceptionEventArgs e)
            {
                MessageBox.Show(e.Exception.Message,"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }

    在C中,使用"using"确保对象在超出范围时被释放。即

    1
    2
    3
    4
    5
    6
    7
            using(IDataReader results = DbManager.ExecuteQuery(dbCommand, transaction))
            {
                while (results.Read())
                {
                    //do something
                }
            }

    另外,在强制转换后检查空值

    1
    2
    3
    4
    5
            MyObject obj = this.bindingSource.Current as MyObject;
            if (MyObject != null)
            {
               // do something
            }

    此外,我尽可能使用枚举,以避免硬编码、打字错误,并在需要时提供简单的重命名,即。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
        private enum MyTableColumns
    {
        UserID,
        UserName
    }

    private enum StoredProcedures
    {
        usp_getMyUser,
        usp_doSomething
    }

    public static MyUser GetMyUser(int userID)
    {
        List<SqlParameter> spParameters = new List<SqlParameter>();

        spParameters.Add(new SqlParameter(MyTableColumns.UserID.ToString(), userID));


        return MyDB.GetEntity(StoredProcedures.usp_getMyUser.ToString(), spParameters, CommandType.StoredProcedure);
    }

    C中:而不是这个:

    1
    if( str==null )

    这样做:

    1
    if( String.IsNullOrEmpty(str) )


    在c中,使用typarse而不是解析值类型,以避免出现格式异常、溢出异常等异常,当然,也可以避免为相同的类型编写try块。

    坏代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    string numberText ="123"; // or any other invalid value

    public int GetNumber(string numberText)
      {
      try
      {
         int myInt = int.Parse(numberText);
         return myInt;
      }
      catch (FormatException)
      {
        //log the error if required
         return 0;
       }
      catch (OverflowException)
      {
         return 0;
      }
    }

    良好的代码(如果不想处理错误)

    1
    2
    3
    4
    5
    6
    string numberText ="123"; // or any other invalid value
    public int GetNumber(string numberText, int defaultReturnValue)
      {
        int myInt;
        return ( int.TryParse(numberText, out myInt) ) ?  myInt : defaultReturnValue;
    }

    几乎所有值类型都可以这样做,例如:boolean.typarse、int16.typarse、decimal.typarse等。


    而在Java中,VAR等于("什么"),我做"什么"。这样,如果var为空,就不必担心nullpointer异常。这在处理URL参数等问题时非常有用。


    不需要去处理语言的局限性,这是我在程序逻辑中可以使用的最好的防御方法。有时,当事情应该停止时,更容易陈述。

    例如,您有这种循环:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    while(1)
    {
      // some codes here

      if(keypress == escape_key || keypress == alt_f4_key
         || keypress == ctrl_w_key || keypress == ctrl_q_key) break;

      // some codes here
    }

    如果要将条件放在循环头上,而不是与语言争用until构造,只需逐字复制条件并加上感叹号:

    1
    2
    3
    4
    5
    while(! (keypress == escape_key || keypress == alt_f4_key
         || keypress == ctrl_w_key || keypress == ctrl_q_key) )
    {
        // some codes here
    }

    在C派生语言中没有构造,所以只需执行上面的操作,否则就可以这样做(可能在C/C++中,使用y*定义;-)

    1
    2
    3
    4
    5
    until(keypress == escape_key || keypress == alt_f4_key
         || keypress == ctrl_w_key || keypress == ctrl_q_key)
    {
        // some codes here
    }


    • 从代码执行SQL查询时,始终使用占位符
    • mysql对DELETE语句有一个有用的非标准扩展:DELETE FROM sometable WHERE name IS LIKE 'foo%' LIMIT 1。这样你就不会把整张桌子都擦干净以免出错。

    语言不可知论:问题:报告和处理一个整体的部分。每当显示计算和百分比时,我总是保留一个运行总数,并且对于最后一个条目,其值的计算与其余的不一样,而是从100.00中减去运行总数。以这种方式,如果某个感兴趣的一方选择将所有的component百分比相加,那么他们将精确地加到100.00。


    不太聪明,也许是个不错的练习。在C/C++中:

    总是从底部的函数返回,而不是在中间。唯一的例外是检查所需参数是否为空;它总是首先出现并立即返回(否则我只会在顶部编写一个很大的"if"条件,这看起来很愚蠢)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int MyIntReturningFuction(char *importantPointer)
    {
        int intToReturn = FAILURE;
        if (NULL == importantPointer)
        {
            return FAILURE;
        }
        // Do code that will set intToReturn to SUCCESS (or not).
        return intToReturn;
    }

    我已经看到很多关于为什么这并不重要的争论,但对我来说最好的争论只是经验。我经常会挠头,问"为什么这个函数底部附近的断点没有被击中?"结果发现除了我以外的其他人在上面的某个地方放了一个退货单(而且通常会改变一些本应该单独处理的情况)。

    我还发现,拥有这样简单的规则会使我成为一个更加一致的编码器。我从来没有特别违反过这条规则,所以有时我不得不考虑处理事情的其他方法(比如清理内存等等)。到目前为止,它一直是更好的。