函数应该返回null还是空对象?

时间:2009-10-26 18:44:06

标签: c# .net function return-value

从函数返回数据时,最佳实践是什么?返回Null或空对象更好吗?为什么要一个人做另一个呢?

考虑一下:

public UserEntity GetUserById(Guid userId)
{
     //Imagine some code here to access database.....

     //Check if data was returned and return a null if none found
     if (!DataExists)
        return null; 
        //Should I be doing this here instead? 
        //return new UserEntity();  
     else
        return existingUserEntity;
}

让我们假装在这个程序中有效的案例,数据库中没有该GUID的用户信息。我想在这种情况下抛出异常是不合适的?此外,我的印象是异常处理会影响性能。

31 个答案:

答案 0 :(得分:208)

如果您打算表明没有可用数据,则返回null通常是最好的选择。

空对象意味着返回了数据,而返回null则清楚地表明没有返回任何内容。

此外,如果您尝试访问对象中的成员,则返回null将导致null异常,这对于突出显示错误代码非常有用 - 尝试访问任何成员都没有意义。访问空对象的成员不会失败意味着错误可能无法发现。

答案 1 :(得分:44)

这取决于对你的案件最有意义的事情。

返回null是否有意义,例如“没有这样的用户存在”?

或者创建默认用户是否有意义?当你可以安全地假设如果用户不存在时,这就是最有意义的,调用代码打算在他们要求时存在一个。

如果调用代码要求用户ID无效,那么抛出异常(la“FileNotFound”)是否有意义?

然而 - 从关注点分离/ SRP的角度来看,前两个更正确。并且技术上第一个是最正确的(但只有一个头发) - GetUserById应该只负责一件事 - 获取用户。通过返回其他内容来处理自己的“用户不存在”案例可能违反了SRP。如果您选择抛出异常,则分成不同的检查 - bool DoesUserExist(id)是合适的。

基于以下广泛的评论:如果这是API级别的设计问题,此方法可能类似于“OpenFile”或“ReadEntireFile”。我们从一些存储库“打开”用户并从结果数据中保存对象。在这种情况下,可能异常。它可能不是,但它可能是。

所有方法都是可以接受的 - 它取决于API /应用程序的更大上下文。

答案 2 :(得分:30)

就个人而言,我使用NULL。它清楚地表明没有数据可以返回。但有些情况下Null Object可能有用。

答案 3 :(得分:27)

如果返回类型是数组,则返回空数组,否则返回null。

答案 4 :(得分:12)

如果特定合同被破坏,您应该抛出例外(仅限) 在您的具体示例中,根据已知的Id请求UserEntity,如果缺少(已删除)用户是预期的案例,则将取决于事实。如果是,则返回null,但如果不是预期的情况,则抛出异常 请注意,如果函数被调用UserEntity GetUserByName(string name),它可能不会抛出但返回null。在这两种情况下,返回一个空的UserEntity将是无益的。

对于字符串,数组和集合,情况通常是不同的。我记得一些指南形式MS,方法应该接受null作为'空'列表,但返回零长度而不是null的集合。字符串相同。请注意,您可以声明空数组:int[] arr = new int[0];

答案 5 :(得分:11)

这是一个业务问题,取决于具有特定Guid Id的用户是否是此函数的预期正常用例,或者是否会阻止应用程序成功完成此方法的任何功能的异常提供用户对象...

如果它是“例外”,因为缺少具有该ID的用户将阻止应用程序成功完成它正在执行的任何功能,(假设我们正在为客户创建发票,我们已将产品发送至...),然后这种情况应抛出ArgumentException(或其他一些自定义异常)。

如果缺少的用户没问题,(调用此函数的潜在正常结果之一)则返回null ....

编辑:(在另一个答案中解决亚当的评论)

如果应用程序包含多个业务流程,其中一个或多个业务流程需要用户才能成功完成,并且其中一个或多个业务流程可以在没有用户的情况下成功完成,那么应该在调用堆栈中进一步抛出异常,更靠近需要用户调用此执行线程的业务流程。此方法与该点(抛出异常)之间的方法应该只传达没有用户存在(null,boolean,无论什么 - 这是一个实现细节)。

但是如果应用程序中的所有进程都需要用户,我仍然会在此方法中抛出异常......

答案 6 :(得分:10)

我个人会返回null,因为这就是我期望DAL / Repository层行动的方式。

如果它不存在,请不要返回任何可被解释为成功获取对象的内容,null在这里工作得很漂亮。

最重要的是要在你的DAL / Repos Layer中保持一致,这样你就不会对如何使用它感到困惑。

答案 7 :(得分:7)

我倾向于

  • return null如果对象ID不存在,则事先不知道 是否存在。
  • throw如果 时对象ID不存在。

我用这三种方法区分这两种情况。 第一:

Boolean TryGetSomeObjectById(Int32 id, out SomeObject o)
{
    if (InternalIdExists(id))
    {
        o = InternalGetSomeObject(id);

        return true;
    }
    else
    {
        return false;
    }
}

第二

SomeObject FindSomeObjectById(Int32 id)
{
    SomeObject o;

    return TryGetObjectById(id, out o) ? o : null;
}

第三

SomeObject GetSomeObjectById(Int32 id)
{
    SomeObject o;

    if (!TryGetObjectById(id, out o))
    {
        throw new SomeAppropriateException();
    }

    return o;
}

答案 8 :(得分:6)

另一种方法涉及传入将对值进行操作的回调对象或委托。如果未找到值,则不会调用回调。

public void GetUserById(Guid id, UserCallback callback)
{
    // Lookup user
    if (userFound)
        callback(userEntity);  // or callback.Call(userEntity);
}

当您希望避免在代码中进行空检查时,这种方法很有效,并且当找不到值不是错误时。如果您需要任何特殊处理,也可以在没有找到对象的情况下提供回调。

public void GetUserById(Guid id, UserCallback callback, NotFoundCallback notFound)
{
    // Lookup user
    if (userFound)
        callback(userEntity);  // or callback.Call(userEntity);
    else
        notFound(); // or notFound.Call();
}

使用单个对象的相同方法可能如下所示:

public void GetUserById(Guid id, UserCallback callback)
{
    // Lookup user
    if (userFound)
        callback.Found(userEntity);
    else
        callback.NotFound();
}

从设计的角度来看,我真的很喜欢这种方法,但缺点是使用不支持一流功能的语言使调用站点更加庞大。

答案 9 :(得分:4)

我更喜欢null,因为它与空合并运算符(??)兼容。

答案 10 :(得分:4)

我们使用CSLA.NET,它认为数据提取失败应该返回一个“空”对象。这实际上非常烦人,因为它要求检查obj.IsNew而不是obj == null是否符合惯例。

正如前面提到的海报所述, null返回值会导致代码立即失败,从而降低由空对象引起隐身问题的可能性。

就个人而言,我认为null更优雅。

这是一个非常常见的情况,我很惊讶这里的人们对此感到惊讶:在任何Web应用程序中,数据通常都是使用查询字符串参数获取的,这显然会被破坏,因此需要开发人员处理“没找到”。

您可以通过以下方式处理:

if (User.Exists(id)) {
  this.User = User.Fetch(id);
} else {
  Response.Redirect("~/notfound.aspx");
}

...但这是每次对数据库的额外调用,这可能是高流量页面上的问题。鉴于:

this.User = User.Fetch(id);

if (this.User == null) {
  Response.Redirect("~/notfound.aspx");
}

...只需要一个电话。

答案 11 :(得分:4)

我会说返回null而不是空对象。

但是你在这里提到的具体实例, 您正在按用户ID搜索用户,即排序 该用户的关键,在这种情况下,我可能想要 如果没有用户实例实例,则抛出异常 找到。

这是我通常遵循的规则:

  • 如果通过主键操作找不到任何结果, 抛出ObjectNotFoundException。
  • 如果根据任何其他标准找不到任何结果, return null。
  • 如果找不到可能返回多个对象的非关键条件的查找结果 返回一个空集合。

答案 12 :(得分:3)

我亲自返回该对象的默认实例。原因是我希望该方法返回零到多或零到一(取决于方法的目的)。使用这种方法,它是任何类型的错误状态的唯一原因是,如果该方法没有返回任何对象,并且总是期望(根据一对多或单数返回)。

关于这是一个商业领域问题的假设 - 我只是不从等式的那一侧看到它。返回类型的规范化是一个有效的应用程序架构问题。至少,它是编码实践标准化的主题。我怀疑有一个业务用户会说“在场景X中,只是给他们一个空”。

答案 13 :(得分:3)

在我们的Business Objects中,我们有两个主要的Get方法:

为了在上下文中保持简单,或者您提出疑问:

// Returns null if user does not exist
public UserEntity GetUserById(Guid userId)
{
}

// Returns a New User if user does not exist
public UserEntity GetNewOrExistingUserById(Guid userId)
{
}

获取特定实体时使用第一种方法,第二种方法专门用于在网页上添加或编辑实体时使用。

这使我们能够在使用它们的环境中充分利用这两个世界。

答案 14 :(得分:3)

在这种情况下,最好的情况是在没有这样的用户的情况下返回“null”。同时使您的方法保持静态。

编辑:

通常这样的方法是某些“User”类的成员,并且无法访问其实例成员。在这种情况下,该方法应该是静态的,否则您必须创建一个“User”实例,然后调用GetUserById方法,该方法将返回另一个“User”实例。同意这是令人困惑的。但是如果GetUserById方法是某个“DatabaseFactory”类的成员 - 将它留作实例成员没有问题。

答案 15 :(得分:3)

它会根据上下文而有所不同,但如果我正在寻找一个特定的对象(如你的例子中),我通常会返回null,如果我正在寻找一组对象但是没有,则返回一个空集合

如果你在代码中犯了一个错误并且返回null会导致空指针异常,那么越早捕获越好。如果您返回一个空对象,初始使用它可能会有效,但您可能会在以后遇到错误。

答案 16 :(得分:3)

我是法国IT学生,请原谅我的英语不好。在我们的类中,我们被告知这样的方法永远不应该返回null,也不应该返回空对象。该方法的用户应该在尝试获取之前首先检查他正在寻找的对象。

使用Java,我们被要求在任何可能返回null的方法的开头添加assert exists(object) : "You shouldn't try to access an object that doesn't exist";来表达“前提条件”(我不知道英文单词是什么)。

IMO这真的不容易使用,但这就是我正在使用的东西,等待更好的东西。

答案 17 :(得分:3)

如果找不到用户的情况,并且你想根据环境以各种方式处理(有时抛出异常,有时替换空用户),你也可以使用接近F#的东西。 Option或Haskell的Maybe类型,明确地将“无价值”案例与“找到的东西!”分开。数据库访问代码可能如下所示:

public Option<UserEntity> GetUserById(Guid userId)
{
 //Imagine some code here to access database.....

 //Check if data was returned and return a null if none found
 if (!DataExists)
    return Option<UserEntity>.Nothing; 
 else
    return Option.Just(existingUserEntity);
}

可以像这样使用:

Option<UserEntity> result = GetUserById(...);
if (result.IsNothing()) {
    // deal with it
} else {
    UserEntity value = result.GetValue();
}

不幸的是,每个人似乎都在推出类似他们自己的类型。

答案 18 :(得分:2)

有趣的问题,我认为没有“正确”的答案,因为它总是取决于您的代码的责任。您的方法是否知道找不到数据是否有问题?在大多数情况下,答案是“否”,这就是为什么返回null并让调用者处理他的情况是完美的。

将抛出方法与返回null的方法区分开来的一种好方法是在团队中找到一个约定:如果没有任何东西可以获得,那么说它们“获取”某些内容的方法应该抛出异常。可能返回null的方法可能会有不同的命名,可能是“Find ...”。

答案 19 :(得分:2)

我不想从任何方法返回null,而是使用Option功能类型。不返回任何结果的方法返回一个空的Option,而不是null。

此外,此类无法返回结果的方法应通过其名称指明。我通常在方法名称的开头放置Try或TryGet或TryFind来表示它可能返回一个空结果(例如TryFindCustomer,TryLoadFile等)。

这让调用者可以应用不同的技术,例如收集流水线(参见Martin Fowler的Collection Pipeline)。

这是另一个使用返回Option而不是null来减少代码复杂性的示例:How to Reduce Cyclomatic Complexity: Option Functional Type

答案 20 :(得分:2)

我通常会返回null。它提供了一种快速简便的机制,可以检测是否有东西被搞砸而没有抛出异常并且在整个地方使用了大量的try / catch。

答案 21 :(得分:2)

如果返回的对象是可以迭代的东西,我会返回一个空对象,这样我就不必先测试null了。

示例:

bool IsAdministrator(User user)
{
    var groupsOfUser = GetGroupsOfUser(user);

    // This foreach would cause a run time exception if groupsOfUser is null.
    foreach (var groupOfUser in groupsOfUser) 
    {
        if (groupOfUser.Name == "Administrators")
        {
            return true;
        }
    }

    return false;
}

答案 22 :(得分:2)

对于集合类型,我将返回一个Empty Collection,对于所有其他类型,我更喜欢使用NullObject模式返回一个实现与返回类型相同的接口的对象。有关模式的详细信息,请查看link text

使用NullObject模式,这将是: -

public UserEntity GetUserById(Guid userId)

{      //想象一下这里的一些代码来访问数据库.....

 //Check if data was returned and return a null if none found
 if (!DataExists)
    return new NullUserEntity(); //Should I be doing this here instead? return new UserEntity();  
 else
    return existingUserEntity;

}

class NullUserEntity: IUserEntity { public string getFirstName(){ return ""; } ...} 

答案 23 :(得分:2)

原谅我的伪php /代码。

我认为这实际上取决于结果的预期用途。

如果您打算编辑/修改返回值并保存它,则返回一个空对象。这样,您可以使用相同的函数来填充新对象或现有对象上的数据。

假设我有一个带有主键和数据数组的函数,用数据填充行,然后将结果记录保存到数据库。由于我打算用任何方式用我的数据填充对象,因此从getter获取一个空对象可能是一个巨大的优势。这样,我可以在任何一种情况下执行相同的操作。无论如何都使用getter函数的结果。

示例:

function saveTheRow($prim_key, $data) {
    $row = getRowByPrimKey($prim_key);

    // Populate the data here

    $row->save();
}

在这里我们可以看到同一系列的操作操纵了这种类型的所有记录。

但是,如果返回值的最终意图是读取并对数据执行某些操作,那么我将返回null。这样,我可以非常快速地确定是否没有返回数据并向用户显示相应的消息。

通常,我会在我的函数中捕获检索数据的异常(因此我可以记录错误消息等等)然后直接从catch返回null。通常对最终用户来说问题是什么并不重要,所以我发现最好将我的错误记录/处理直接封装在获取数据的函数中。如果您在任何大型公司维护共享代码库,这是特别有益的,因为即使是最懒的程序员也可以强制进行正确的错误记录/处理。

示例:

function displayData($row_id) {
    // Logging of the error would happen in this function
    $row = getRow($row_id);
    if($row === null) {
        // Handle the error here
    }

    // Do stuff here with data
}

function getRow($row_id) {
 $row = null;
 try{
     if(!$db->connected()) {
   throw excpetion("Couldn't Connect");
  }

  $result = $db->query($some_query_using_row_id);

  if(count($result) == 0 ) {
   throw new exception("Couldn't find a record!");
  }

  $row = $db->nextRow();

 } catch (db_exception) {
  //Log db conn error, alert admin, etc...
  return null; // This way I know that null means an error occurred
 }
 return $row;
}

这是我的一般规则。到目前为止它运作良好。

答案 24 :(得分:2)

以别人的方式说出别人所说的话......

例外情况适用于特殊情况

如果这个方法是纯数据访问层,我会说给定一些参数包含在一个select语句中,它会期望我找不到构建对象的任何行,因此返回null将是可以接受,因为这是数据访问逻辑。

另一方面,如果我希望我的参数反映一个主键,我只能返回一个行,如果我有多个回来,我会抛出异常。 0表示返回null,2表示不是。

现在,如果我有一些针对LDAP提供程序进行检查的登录代码,则检查数据库以获取更多详细信息,并且我预计这些应该始终保持同步,我可能会抛出异常。正如其他人所说,这是商业规则。

现在我要说这是一般规则。有时您可能希望打破这种局面。但是,我使用C#(很多)和Java(有点那样)的经验和实验告诉我,处理异常比处理可预测的问题更有代价 。逻辑。在某些情况下,我说的话要贵2到3个数量级。所以,如果你的代码可能最终出现在循环中,那么我会建议返回null并测试它。

答案 25 :(得分:1)

更多的肉研磨:假设我的DAL按照一些人的建议为GetPersonByID返回NULL。如果收到NULL,我的(相当薄)BLL应该怎么办?将NULL传递给up并让最终消费者担心它(在本例中是一个ASP.Net页面)?让BLL抛出异常怎么样?

BLL可能正被ASP.Net和Win App或其他类库使用 - 我认为期望最终消费者本质上“知道”方法GetPersonByID返回null是不公平的(除非使用null类型,我想)。

我的看法(因为它值得)是我的DAL如果没有找到则返回NULL。对于某些物体,这没关系 - 它可能是0:许多事物列表,所以没有任何东西是好的(例如,最喜欢的书籍列表)。在这种情况下,我的BLL返回一个空列表。对于大多数单个实体的东西(例如用户,帐户,发票),如果我没有,那那肯定是一个问题而且抛出一个代价高昂的例外。然而,看到通过应用程序先前给出的唯一标识符检索用户应始终返回用户,该异常是“正确的”异常,因为它是例外。 BLL(ASP.Net,f'rinstance)的最终消费者只期望事情变得困难,因此将使用未处理的异常处理程序,而不是在try-catch块中包含对GetPersonByID的每次调用。

如果我的方法存在明显的问题,请告诉我,因为我总是热衷于学习。正如其他海报所说,例外是昂贵的事情,“先检查”方法是好的,但例外情况应该只是 - 例外。

我很喜欢这篇文章,很多关于“依赖”场景的好建议: - )

答案 26 :(得分:1)

对于代码库的运行状况,我认为函数不应该返回null。我可以想到几个原因:

将会有大量的保护条款处理空引用if (f() != null)

什么是null,是接受的答案还是问题? null是否为特定对象的有效状态? (假设您是代码的客户端)。我的意思是所有引用类型都可以为null,但它们应该是吗?

随着代码库的增长,null闲置几乎总会给出一些意想不到的NullRef异常。

有一些解决方案,tester-doer pattern或实现函数式编程的option type

答案 27 :(得分:0)

我对需要两种方法的答案数量(整个网络)感到困惑:“IsItThere()”方法和“GetItForMe()”方法因此导致竞争条件。一个函数返回null,将它赋值给变量,并在一次测试中检查变量是否为Null有什么问题?我以前的C代码充满了

if(NULL!=(variable = function(arguments ...))){

所以你得到一个变量的值(或null),结果全部一次。这个成语被遗忘了吗?为什么呢?

答案 28 :(得分:0)

我同意这里的大多数帖子,这些帖子往往null

我的理由是生成具有非可空属性的空对象可能会导致错误。例如,具有int ID属性的实体的初始值为ID = 0,这是一个完全有效的值。在某些情况下,该对象是否应该保存到数据库中,这将是一件坏事。

对于任何带有迭代器的东西,我会总是使用空集合。像

这样的东西
foreach (var eachValue in collection ?? new List<Type>(0))
在我看来,

是代码味道。集合属性不应该为null。

边缘情况为String。很多人说,String.IsNullOrEmpty并不是必需的,但你不能总是区分空字符串和null。此外,某些数据库系统(Oracle)根本不会区分它们(''存储为DBNULL),因此您不得不平等地处理它们。原因是,大多数字符串值来自用户输入或外部系统,而文本框和大多数交换格式都没有''null的不同表示形式。因此,即使用户想要删除一个值,他也不能做任何事情而不是清除输入控件。如果您的DBMS不是oracle,那么可空和不可空nvarchar数据库字段的区别是值得怀疑的 - 允许''的必填字段很奇怪,您的UI永远不允许这样做,所以你的约束不映射。 所以,在我看来,这里的答案始终是平等对待它们。

关于您的例外与表现问题: 如果你抛出一个你无法在程序逻辑中完全处理的异常,你必须在某个时候中止你的程序正在做什么,并要求用户重做他刚刚做的任何事情。在这种情况下,catch的性能损失实际上是您最不担心的 - 必须要求用户是房间里的大象(这意味着重新渲染整个UI,或通过互联网发送一些HTML) )。因此,如果你不遵循“Program Flow with Exceptions”的反模式,请不要打扰,只要它有意义就抛出一个。即使在边缘情况下,例如“验证异常”,性能也不是问题,因为在任何情况下你都必须再次询问用户。

答案 29 :(得分:0)

异步TryGet模式:

对于同步方法,我相信@Johann Gerell's answer是在所有情况下都可以使用的 模式。

但是带有out参数的TryGet模式不适用于异步方法。

使用C#7的元组文字,您现在可以执行以下操作:

async Task<(bool success, SomeObject o)> TryGetSomeObjectByIdAsync(Int32 id)
{
    if (InternalIdExists(id))
    {
        o = await InternalGetSomeObjectAsync(id);

        return (true, o);
    }
    else
    {
        return (false, default(SomeObject));
    }
}

答案 30 :(得分:-1)

如果您使用无效的用户ID调用该代码,则应该抛出异常。如果它不是特殊情况,那么您实际上在做的是使用“getter”方法来测试用户是否存在。这就像尝试打开文件以查看它是否存在(让我们坚持使用c#/ java)而不是使用exists方法,或者尝试通过查看返回值而不是通过查看返回值来查看字典元素并查看它们是否存在首先使用“包含”方法。

因此,您可能需要使用“存在”之类的其他方法来首先检查是否有这样的用户。除非您有真正的性能问题,否则异常的执行绝对不是完全不使用它们的理由。