关于c#:函数应该返回null还是空对象?

Should functions return null or an empty object?

从函数返回数据时的最佳实践是什么?返回空对象还是空对象更好?为什么一个要做一个而另一个呢?

考虑一下:

1
2
3
4
5
6
7
8
9
10
11
12
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的用户信息。我想在这种情况下抛出异常是不合适的??我还认为异常处理会影响性能。


如果您打算指示没有可用的数据,返回空值通常是最好的方法。

空对象表示已返回数据,而返回空对象则表示未返回任何数据。

此外,如果尝试访问对象中的成员,返回空值将导致空异常,这对于突出显示错误代码很有用-尝试访问无内容的成员毫无意义。访问空对象的成员不会失败,这意味着可以发现错误。


这取决于什么对你的案件最有意义。

返回空值是否有意义,例如"不存在这样的用户"?

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

或者,如果调用代码要求用户使用无效的ID,那么抛出异常(la"filenotfound")是否有意义?

然而-从关注点/SRP的分离来看,前两个更为正确。从技术上讲,第一个是最正确的(但只有一根头发),getuserbyid应该只负责一件事——获取用户。通过返回其他内容来处理自己的"用户不存在"情况可能违反SRP。如果您确实选择抛出异常,则将其分为不同的检查-bool DoesUserExist(id)是合适的。

基于下面的大量评论:如果这是一个API级的设计问题,那么这个方法可以类似于"openfile"或"readentirefile"。我们正在从一些存储库中"打开"一个用户,并从结果数据中为对象补水。在这种情况下,异常可能是适当的。可能不是,但可能是。

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


就个人而言,我使用空值。很明显,没有要返回的数据。但有时空对象可能有用。


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


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

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


这是一个业务问题,取决于具有特定guid id ID的用户的存在是否是此函数的预期正常使用情况,或者是一个异常情况,它将阻止应用程序成功完成此方法向用户对象提供的任何函数…

如果这是一个"异常",因为缺少具有该ID的用户将阻止应用程序成功完成它正在执行的任何功能(例如,我们正在为已将产品发送给……的客户创建发票),那么这种情况应该引发一个ArgumentException(或其他自定义异常)。

如果丢失的用户正常,(调用此函数的一个潜在正常结果),则返回空值….

编辑:(在另一个答案中处理来自亚当的评论)

如果应用程序包含多个业务流程,其中一个或多个业务流程需要用户才能成功完成,并且其中一个或多个业务流程可以在没有用户的情况下成功完成,则应将异常进一步向调用堆栈中抛出,靠近需要用户调用此执行线程的业务流程的位置。稀释。此方法与该点(引发异常的点)之间的方法应该只进行通信,说明不存在任何用户(空、布尔值,无论什么——这是一个实现细节)。

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


我个人会返回空值,因为这是我希望DAL/Repository层的行为方式。

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

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


我倾向于

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
Boolean TryGetSomeObjectById(Int32 id, out SomeObject o)
{
    if (InternalIdExists(id))
    {
        o = InternalGetSomeObject(id);

        return true;
    }
    else
    {
        return false;
    }
}

第二:

1
2
3
4
5
6
SomeObject FindSomeObjectById(Int32 id)
{
    SomeObject o;

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

第三:

1
2
3
4
5
6
7
8
9
10
11
SomeObject GetSomeObjectById(Int32 id)
{
    SomeObject o;

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

    return o;
}


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

1
2
3
4
5
6
public void GetUserById(Guid id, UserCallback callback)
{
    // Lookup user
    if (userFound)
        callback(userEntity);  // or callback.Call(userEntity);
}

当您希望避免在代码中进行空检查,而不查找值并不是错误时,这会很好地工作。如果需要特殊处理,还可以为找不到对象时提供回调。

1
2
3
4
5
6
7
8
public void GetUserById(Guid id, UserCallback callback, NotFoundCallback notFound)
{
    // Lookup user
    if (userFound)
        callback(userEntity);  // or callback.Call(userEntity);
    else
        notFound(); // or notFound.Call();
}

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

1
2
3
4
5
6
7
8
public void GetUserById(Guid id, UserCallback callback)
{
    // Lookup user
    if (userFound)
        callback.Found(userEntity);
    else
        callback.NotFound();
}

从设计的角度来看,我真的很喜欢这种方法,但是有一个缺点,那就是在不支持第一类函数的语言中,使调用站点变得更庞大。


我更喜欢null,因为它与空合并操作符(??兼容。


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

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

这是我通常遵循的规则:

  • 如果在按主键查找操作上找不到结果,引发ObjectNotFoundException。
  • 如果根据任何其他标准在查找中未找到结果,返回空值。
  • 如果按可能返回多个对象的非键条件在查找上找不到结果返回空集合。


我们使用csla.net,它认为失败的数据获取应该返回一个"空"对象。这实际上相当烦人,因为它要求惯例检查obj.IsNewobj == null是否更高。

正如前面提到的,空返回值将导致代码直接失效,从而降低了由空对象引起的隐藏问题的可能性。

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

这是一个非常常见的情况,我很惊讶这里的人们似乎对此感到惊讶:在任何Web应用程序中,数据通常是使用一个querystring参数获取的,这显然会被破坏,因此要求开发人员处理"未找到"的发生率。

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

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

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

1
2
3
4
5
this.User = User.Fetch(id);

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

…只需要一个电话。


在我们的业务对象中,我们有两种主要的获取方法:

为了让事情简单化,或者你会质疑:

1
2
3
4
5
6
7
8
9
// 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)
{
}

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

这使我们能够在使用它们的环境中拥有两个世界中最好的一个。


它将根据上下文而变化,但如果我在查找一个特定对象(如您的示例中所示),则通常返回空值;如果我在查找一组对象但没有,则返回空集合。

如果您在代码中犯了一个错误,并且返回空值会导致空指针异常,那么越快发现这一点越好。如果返回一个空对象,最初使用它可能会起作用,但稍后可能会出错。


在这种情况下,如果没有这样的用户,最好返回"空"。同时使您的方法是静态的。

编辑:

通常,像这样的方法是一些"用户"类的成员,并且不能访问其实例成员。在这种情况下,该方法应该是静态的,否则必须创建一个"user"实例,然后调用getuserbyid方法,该方法将返回另一个"user"实例。同意这是令人困惑的。但是,如果getUserByID方法是某个"databaseFactory"类的成员,那么将其保留为实例成员也没问题。


我是个法语IT学生,所以请原谅我的英语不好。在我们的类中,有人告诉我们这样的方法不应该返回空值,也不应该返回空对象。这个方法的用户应该先检查他要查找的对象是否存在,然后再尝试获取它。

使用Java,我们被要求在任何返回null的方法的开始时添加EDCOX1 0,以表示"前提条件"(我不知道英语中的单词是什么)。

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


我亲自返回对象的默认实例。原因是我希望方法返回0到多或0到1(取决于方法的用途)。使用这种方法,它将成为任何类型的错误状态的唯一原因是,如果该方法没有返回任何对象,并且总是被期望返回(以一对多或奇异返回的形式)。

至于假设这是一个业务领域的问题,我只是没有从等式的那一边看到它。返回类型的规范化是一个有效的应用程序体系结构问题。至少,它是编码实践中标准化的主题。我怀疑是否有一个业务用户会说"在场景X中,只给他们一个空值"。


如果用户未被发现的情况经常出现,并且您希望根据情况以各种方式处理(有时抛出异常,有时替换空用户),您也可以使用接近f's Option或haskell's Maybe类型的东西,这明确地将"无值"情况与"发现了什么!"。数据库访问代码可能如下所示:

1
2
3
4
5
6
7
8
9
10
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);
}

像这样使用:

1
2
3
4
5
6
Option<UserEntity> result = GetUserById(...);
if (result.IsNothing()) {
    // deal with it
} else {
    UserEntity value = result.GetValue();
}

不幸的是,每个人似乎都有自己的风格。


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

也许区分抛出方法和返回空值方法的一个好方法是在您的团队中找到一个约定:如果没有要获取的东西,那么说它们"获取"的方法应该抛出一个异常。可能返回空值的方法可以用不同的名称命名,可能改为"find…"。


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

使用nullObject模式,这将是:

1
public UserEntity GetUserById(Guid userId)

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

1
2
3
4
5
 //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;

}

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

把别人说的话用一种更简练的方式…

例外情况适用于特殊情况。

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

另一方面,如果我期望我的参数反映一个主键,而我应该只返回一行,如果我返回多行,我将抛出一个异常。0可以返回空值,2不可以。

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

现在我要说这是一般规则。有时候你可能会想打破它。然而,我的经验和实验用C(很多)和Java(有点)告诉我,处理异常的代价比通过条件逻辑来处理可预测的问题要昂贵得多。我说的是2到3个数量级的调子,在某些情况下更贵。所以,如果您的代码可能以循环结束,那么我建议您返回空值并测试它。


我通常返回空值。它提供了一种快速而简单的机制,可以检测出是否有什么东西出错,而不会抛出异常,并在整个地方使用大量的尝试/捕获。


请原谅我的伪PHP/代码。

我认为这真的取决于结果的预期用途。

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

假设我有一个函数,它获取主键和数据数组,用数据填充行,然后将结果记录保存到数据库。因为我打算用我的数据来填充这个对象,所以从getter中获取一个空对象可能是一个巨大的优势。这样,在任何情况下我都可以执行相同的操作。不管怎样,都使用getter函数的结果。

例子:

1
2
3
4
5
6
7
function saveTheRow($prim_key, $data) {
    $row = getRowByPrimKey($prim_key);

    // Populate the data here

    $row->save();
}

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

但是,如果返回值的最终目的是读取数据并对其进行处理,那么我将返回空值。这样,我可以很快地确定是否没有返回数据,并向用户显示适当的消息。

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

例子:

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
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;
}

这是我的一般规则。到目前为止效果很好。


我不喜欢从任何方法返回空值,而是使用选项函数类型。不能返回任何结果的方法返回空选项,而不是空选项。

此外,这样的方法不能返回任何结果,应该通过它们的名称来表示这一点。我通常将try或tryget或tryfind放在方法名称的开头,以指示它可能返回空结果(例如,tryfindcustomer、tryloadfile等)。

这样,调用者就可以对结果应用不同的技术,比如收集管道(参见MartinFowler的收集管道)。

下面是另一个示例,其中返回选项而不是空值用于降低代码复杂性:如何降低循环复杂性:选项函数类型


如果返回的对象是可以迭代的对象,那么我将返回一个空对象,这样就不必首先测试空对象。

例子:

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

更多的肉要磨:假设我的DAL为getPersonByID返回一个空值,正如一些人建议的那样。如果收到空值,我的(相当瘦的)BLL应该怎么做?向上传递这个空值,让最终消费者担心它(在本例中,是一个ASP.NET页)?让BLL抛出异常如何?

BLL可能被ASP.NET和Win应用程序或其他类库使用-我认为期望最终消费者本质上"知道"getPersonByID方法返回空值是不公平的(除非使用空值类型,我猜)。

我的看法是,如果没有发现任何东西,我的DAL将返回空值。对于某些对象,这是可以的-它可以是一个0:多的列表,所以没有任何东西是很好的(例如,最喜欢的书的列表)。在这种情况下,我的BLL返回一个空列表。对于大多数单一实体的事情(例如用户、帐户、发票),如果我没有,那么这绝对是个问题,抛出一个昂贵的异常。但是,正如通过应用程序先前提供的唯一标识符检索用户一样,该异常应该始终返回用户,这是一个"适当"的异常,正如它的异常一样。BLL(asp.net,f'rinstance)的最终使用者只希望事情变得复杂,因此将使用未处理的异常处理程序,而不是将对getPersonByID的每个调用包装在一个try-catch块中。

如果我的方法有一个明显的问题,请告诉我,因为我一直渴望学习。正如其他海报所说,例外是昂贵的事情,"先检查"的方法是好的,但例外应该就是这样-例外。

我很喜欢这篇文章,对于"视情况而定"的场景有很多好的建议:—)


为了代码库的健康,我认为函数不应该返回空值。我可以想到几个原因:

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

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

随着代码库的增长,让EDOCX1[0]挂起几乎总是会不时地出现一些意外的nullRef异常。

有一些解决方案,如tester-doer pattern或从功能编程实现option type


异步Tryget模式:

对于同步方法,我相信@johann gerell的答案是在所有情况下使用的模式。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
async Task<(bool success, SomeObject o)> TryGetSomeObjectByIdAsync(Int32 id)
{
    if (InternalIdExists(id))
    {
        o = await InternalGetSomeObjectAsync(id);

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

我对(整个网络上)的答案数量感到困惑,这些答案说您需要两种方法:一种"isitthere()"方法和一种"getitforme()"方法,因此这会导致竞争条件。返回空值、将其分配给变量以及在一个测试中检查变量是否为空值的函数有什么问题?我以前的C代码充满了

如果(NULL!=(变量=函数(参数…)){

所以你可以得到一个变量中的值(或者空值),以及结果。这个成语忘了吗?为什么?


我同意这里大多数的帖子,倾向于null

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

对于任何带有迭代器的内容,我都将始终使用空集合。类似的东西

1
foreach (var eachValue in collection ?? new List<Type>(0))

我觉得代码有味道吗?集合属性不应为空。

边箱是String。许多人说,String.IsNullOrEmpty不是真正必要的,但不能总是区分空字符串和空字符串。此外,有些数据库系统(Oracle)根本无法区分它们(''存储为DBNULL,因此您必须平等地处理它们。原因是,大多数字符串值要么来自用户输入,要么来自外部系统,而文本框和大多数交换格式对于''null都没有不同的表示。因此,即使用户想要删除一个值,他也只能清除输入控件。另外,可以为空和不可以为空的nvarchar数据库字段的区别也很可疑,如果您的DBMS不是Oracle(允许''的强制字段),那么您的UI将永远不允许这样做,因此您的约束不会映射。所以在我看来,这里的答案是,总是平等地对待它们。

关于你关于例外和履行的问题:如果抛出一个不能在程序逻辑中完全处理的异常,则必须在某个时刻中止程序正在执行的任何操作,并要求用户重新执行他刚刚执行的操作。在这种情况下,对catch的性能惩罚实际上是您最不担心的——必须问用户是房间里的大象(这意味着重新呈现整个用户界面,或者通过互联网发送一些HTML)。所以如果你不遵循"程序流有例外"的反模式,就不用麻烦了,只要有意义就扔一个。即使在临界情况下,例如"验证异常",性能也不是问题,因为在任何情况下都必须再次询问用户。


如果您使用无效的用户ID调用该代码是一种特殊情况,那么应该抛出异常。如果不是一种特殊情况,那么您实质上要做的就是使用"getter"方法来测试用户是否存在。这就好比尝试打开一个文件来查看它是否存在(让我们坚持C语言/ Java),而不是使用现有方法,或者试图访问字典元素,通过查看返回值来查看它们是否存在,而不是先使用"包含"方法。

因此,您可能需要使用"exists"等其他方法来首先检查是否存在这样的用户。除非存在真正的性能问题,否则异常的性能绝对不是根本不使用它们的原因。