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

从函数返回数据时最佳实践是什么?是返回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的用户信息。我认为在这种情况下抛出异常是不合适的??另外,我的印象是异常处理会损害性能。

158619 次浏览

如果您打算表示没有可用的数据,返回null通常是最好的主意。

空对象表示已返回数据,而返回null则明确表示没有返回任何数据。

此外,如果试图访问对象中的成员,返回null将导致null异常,这对于突出显示有bug的代码很有用——试图访问没有任何成员的成员是没有意义的。访问空对象的成员不会失败,这意味着bug不会被发现。

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

返回null是否有意义?“不存在这样的用户”?

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

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

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

根据下面大量的评论:如果这是一个api级别的设计问题,这个方法可以类似于"OpenFile"或" readwholfile "。我们正在从某个存储库中“打开”一个用户,并从结果数据中补充对象。异常可能是适用于这种情况。也许不是,但也有可能。

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

我个人会返回null,因为这是我所期望的DAL/Repository层的操作方式。

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

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

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

如果它是一个“异常”,因为缺少具有该Id的用户将阻止应用程序成功完成它正在做的任何功能,(假设我们正在为我们已向其发货的客户创建发票……),那么这种情况应该抛出ArgumentException(或其他一些自定义异常)。

如果没有用户(调用此函数的潜在正常结果之一),则返回空值....

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

如果应用程序包含多个业务流程,其中一个或多个业务流程需要User才能成功完成,而一个或多个业务流程可以在没有用户的情况下成功完成,则异常应该在调用堆栈的更上层抛出,靠近需要User的业务流程正在调用此执行线程的位置。这个方法和那个点(抛出异常的地方)之间的方法应该只是传达不存在用户(null,布尔值,等等——这是一个实现细节)。

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

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

如果你在代码中犯了一个错误,返回null会导致空指针异常,那么越早发现越好。如果您返回一个空对象,最初使用它可能会工作,但稍后可能会得到错误。

就我个人而言,我使用NULL。它清楚地表明没有数据要返回。但在某些情况下,空对象可能是有用的。

我通常返回null。它提供了一种快速而简单的机制来检测是否出现了错误,而不会抛出异常,也不会到处使用大量的try/catch。

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

在这种情况下,如果没有这样的用户,则最好返回“null”。还要使你的方法是静态的。

编辑:

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

你应该抛出一个异常(仅)当一个特定的契约被破坏 在您的特定示例中,请求基于已知Id的UserEntity,这将取决于丢失(删除)用户是否是预期情况。如果是,则返回null,但如果不是预期的情况,则抛出异常 注意,如果函数被调用UserEntity GetUserByName(string name),它可能不会抛出而是返回null。在这两种情况下,返回空的UserEntity都没有帮助。< / p >

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

在我们的业务对象中,我们有两个主要的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)
{
}

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

这使我们能够在使用它们的上下文中两全其美。

我个人返回该对象的默认实例。原因是我希望该方法返回0到多或0到1(取决于该方法的目的)。使用这种方法,它将是任何类型的错误状态的唯一原因是,如果方法不返回任何对象,并且总是期望返回(就一对多或单数返回而言)。

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

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

使用NullObject模式这将是:-

public UserEntity GetUserById(Guid userId)
< p > { //想象一些代码在这里访问数据库.....

 //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 ""; } ...}

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

但是你在这里提到的具体实例, 您正在通过用户id搜索用户,即排序 那个用户的键,在这种情况下,我可能想要 如果没有用户实例,则抛出异常 发现。< / p >

这是我通常遵循的规则:

  • 如果通过主键查找操作没有结果, 李把ObjectNotFoundException。< / >
  • 如果根据任何其他标准没有发现结果, 李返回null。< / >
  • 如果通过可能返回多个对象的非关键条件查找未找到结果 返回空集合

我们使用CSLA。NET,并且它认为失败的数据获取应该返回一个“空”对象。这实际上很烦人,因为它要求检查是否obj.IsNew而不是obj == null的约定。

正如之前的海报所提到的,空返回值将导致代码立即失败,减少了由空对象引起的隐形问题的可能性。

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

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

你可以这样处理:

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

...只需要一个呼叫。

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

把别人说的话用更简洁的方式来表达……

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

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

另一方面,如果我希望参数反映一个主键,并且只返回一个行,如果返回多个行,则抛出异常。0可以返回null, 2则不行。

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

现在我要说这是一个一般规则。有时候你可能想要打破它。然而,我对c#(大量)和Java(少量)的经验和实验告诉我,与通过条件逻辑处理可预测的问题相比,在处理异常方面的性能更昂贵。我说的是在某些情况下要贵2到3个数量级。所以,如果你的代码可能会在循环中结束,那么我建议返回null并测试它。

请原谅我的伪php/代码。

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

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

假设我有一个函数,它接受一个主键和一个数据数组,用数据填充行,然后将结果记录保存到db。因为我打算用我的数据填充对象,所以从getter返回一个空对象可能是一个巨大的优势。这样,我可以在两种情况下执行相同的操作。无论如何都要使用getter函数的结果。

例子:

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


// Populate the data here


$row->save();
}

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

但是,如果返回值的最终目的是读取数据并对数据做一些事情,那么我将返回null。这样,我可以非常快速地确定是否没有返回数据,并向用户显示适当的消息。

通常,我将在检索数据的函数中捕获异常(因此我可以记录错误消息等),然后从捕获中直接返回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;
}

这是我的一般原则。到目前为止,它运行得很好。

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

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

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

如果用户未被找到的情况出现得足够频繁,并且你想根据情况以各种方式处理(有时抛出异常,有时替换为空用户),你也可以使用接近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();
}

不幸的是,每个人似乎都有自己喜欢的类型。

更多的肉要磨:让我们说我的DAL返回一个NULL的GetPersonByID,正如一些建议。我的(相当薄)BLL应该做什么,如果它收到一个NULL?传递NULL,并让最终消费者担心它(在这种情况下,一个ASP。网络页面)?让BLL抛出一个异常怎么样?

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

我的采取(为它的价值)是,我的DAL返回NULL如果没有找到。对于某些对象,这是可以的——它可以是一个0:很多东西的列表,所以没有任何东西是可以的(例如一个最喜欢的书的列表)。在本例中,我的BLL返回一个空列表。对于大多数单一实体的东西(例如用户,帐户,发票),如果我没有一个,那么这肯定是一个问题,并抛出一个代价高昂的异常。然而,由于通过应用程序先前给出的唯一标识符检索用户应该总是返回用户,因此异常是“适当的”异常,因为它是异常的。BLL的最终消费者(ASP。Net, f' instance)只期望事情是很不错的,因此将使用Unhandled Exception Handler,而不是将每个对GetPersonByID的调用包装在一个try - catch块中。

如果我的方法有明显的问题,请让我知道,因为我总是渴望学习。正如其他帖子所说,异常是代价高昂的事情,“先检查”的方法是好的,但异常应该只是例外。

我很喜欢这篇文章,很多关于“视情况而定”的好建议:-)

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

也许区分抛出方法和返回null方法的一个好方法是在您的团队中找到一个约定:如果没有得到任何东西,表示它们“得到”某些东西的方法应该抛出异常。可能返回null的方法可以以不同的方式命名,可能是“Find…”。

如果返回的对象是可以迭代的对象,我将返回一个空对象,这样我就不必首先测试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;
}

还有一种方法涉及传入一个回调对象或委托,它将对值进行操作。如果没有找到值,则不调用回调。

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

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

我倾向于

  • 如果对象id不存在,预先不知道它是否存在应该,则return null
  • 应该存在时,如果对象id不存在,则throw
我用这三种类型的方法来区分这两种场景。 第一:< / p >
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;
}

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

因此,您很可能会使用诸如“exists”之类的额外方法来首先检查是否存在这样的用户。异常的性能绝对不是完全不使用它们的理由,除非您有真正的性能问题。

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

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

因此,您可以同时获得变量中的值(或null)和结果。这个成语已经被遗忘了吗?为什么?

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

而且,不能返回结果的方法应该通过名称来指明。我通常把Try或TryGet或TryFind放在方法名称的开头,以表明它可能返回一个空结果(例如TryFindCustomer, TryLoadFile等)。

这让调用者可以对结果应用不同的技术,比如集合流水线(参见Martin Fowler的收集管道)。

下面是另一个使用返回Option而不是null来降低代码复杂性的示例:如何降低圈复杂度:选项函数类型

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

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

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

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

在我看来是代码的味道。集合属性永远不应该为空。

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

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

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

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

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

随着代码库的增长,使用null几乎总是会不时地给出一些意想不到的NullRef异常。

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

一个异步TryGet模式:

对于同步方法,我认为@Johann Gerell的 回答是在所有情况下使用的模式。

然而,带有out参数的TryGet模式不适用于Async方法。

使用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));
}
}