如果不应该将集合返回给调用者,那么如何将数据集合返回给调用者?

5
我正在编写一个方法,旨在返回填充有配置键和值的字典。构建此字典的方法是动态的,因此我需要将这组键和值作为集合返回(可能是>)。根据我的各种阅读(来源暂时不明),从方法调用中返回集合类型的一般共识是不要
我理解这个策略的原因,我倾向于同意,但在像这样的情况下,我看不到其他选择。这是我的问题:是否有一种方法可以将这些数据返回给调用者,同时遵循这个原则? 编辑:我听到的不允许这种行为的原因是,旨在由客户端消耗(但不修改)的集合或字典类型会暴露太多行为,给人以调用者可以修改该类型的错觉。例如,字典具有Add和Remove方法,以及可变索引器。如果字典中的值是只读的,则这些方法最多是多余的。如果公开了内部集合,并且“所有者”没有预期来自外部源对集合进行更改,则可能会造成进一步的损害。
我听到过其他原因,但我无法立即回忆起它们-这些对我情况最相关。 编辑:更多澄清:我遇到的问题是,我正在构建一个API,因此我无法控制调用此函数的客户端。克隆字典不是问题,但我试图尽可能保持我的API干净。返回具有Add和Remove等方法的字典表明可以或应该修改集合,而这并不是事实。在这里进行修改是没有意义的,因此我不想通过返回类型的接口公开该功能的承诺。 解决方案:为了满足我对清洁API的渴望,我将编写一个自定义的Dictionary类,不会公开可变的Add和Remove方法或set索引器。此类型将不实现,但我将编写一个方法ToDictionary,它将返回中的数据。它将实现IEnumerable >,以便可以访问枚举上的标准LINQ操作。现在我所需要的就是我的自定义字典类型的名称... =)谢谢大家。

1
你在问哪种编程语言? - anon
在我的例子中是C#,但我想这个问题适用于几乎所有具有可变集合类型的语言。 - Erik Forbes
至少在C++中,你可以返回一个值。如果函数的调用者修改了返回的值,那么不会影响其他任何东西。 - anon
这也适用于C#,但我遇到的问题不是技术问题,而是语义问题。我准备处理不是100%最佳的解决方案;我只是想首先排除所有可能性。 - Erik Forbes
对于您提到的似乎需要使用ImmutableDictionary的解决方案,我建议在谷歌上搜索一下这个类,因为它看起来已经被其他人实现过了。 - Chris Marisic
10个回答

10

在方法调用中返回集合类型并不是一个普遍被认同的做法。

这是我第一次听到这种说法,对我来说似乎是一个愚蠢的限制。

我理解这个政策的原因。

那它们是什么呢?

编辑: 你提到的反对返回集合的理由是特定潜在问题,可以通过返回只读包装器来具体解决,而不需要全面限制返回集合。但是据我了解,该集合实际上是由方法构建的 - 在这种情况下,调用者所做的更改不会影响任何其他内容,因此您不必过于担心,也不应过度限制调用者对专门为他创建的对象的使用。


我理解这个问题不是技术问题,而是语义问题。如果我不打算支持变异,我就不想返回暗示变异的类型。 - Erik Forbes
返回的对象支持变异会有什么不好呢? - Michael Borgwardt
在这种情况下,因为我返回的字典突变没有语义意义。并不是说它是“不好的”,只是不够纯粹。 - Erik Forbes
3
我认为纯度的抽象概念不是添加任意限制的好理由。为什么不能让调用者修改专门为他创建的集合呢?也许这样做很有用。你甚至已经计划编写一个ToDictionary方法来还原你刚刚取消的选项! - Michael Borgwardt

7
这个限制的主要原因是,如果类返回一个成员集合,则会破坏多态性、constness和访问控制。如果你正在构建一个集合来返回,并且类不将其保留为成员,则这不是一个问题。
话虽如此,您可能需要更深入地考虑为什么希望返回此集合。您希望调用类能够对数据执行什么操作?是否可以通过向类添加方法来实现此功能,而不是返回集合(例如,myobj.getvalueFromKey(s)而不是myobj.getdictionary()[s])?是否更适合返回一个仅公开所需信息的对象,而不是简单地返回集合(例如,MyLookupTable MyClass::getLookupTable()而不是IDictionary MyClass::getLookupTable())。
如果您无法控制调用方,并且必须返回给定类型的集合,则应该是成员集合的副本或完全新的集合,调用者不存储它。

这大致是我要找的东西。我的问题在于,我正在构建一个API,所以我无法控制客户端调用此函数。克隆字典不是问题-但我正在尽可能保持我的API干净;返回带有Add和Remove等方法的字典意味着可以或应该修改集合,但情况并非如此。这里的修改是没有意义的,因此我不想通过返回类型的接口展示那种功能的承诺。 - Erik Forbes
此外,我希望避免添加新类型(也就是说,我希望在BCL中有一些我没有听说过的东西可以为我完成这个任务)。然而,看起来我可能没有选择。 - Erik Forbes
如果您正在编写一个尚不存在的API,那么您可以控制调用者。您可以告诉他们需要做什么才能与您的应用程序进行交互。 - Paul Butcher
啊,从那个意义上来说是的;我的意思是,如果我返回一个原始字典,我无法控制他们会如何使用它。=)但我理解你的观点。 - Erik Forbes

1
在我看来,仅当更改返回的集合可能会产生副作用时(例如,多个函数使用同一集合)才会出现返回集合的问题。
如果您只是创建集合而不通过返回集合公开类中的数据,则我认为可以简单地返回字典。
如果集合在代码的其他位置中使用,并且您返回集合的代码不应更改集合,则必须对其进行克隆。

0

针对此问题,请查看ReadOnlyCollection()。更改您的返回类型和最后一个语句为

return new ReadOnlyCollection(whateverYouWereReturningBefore);

0
通常我会返回一个数据数组而不是集合类型。例如,在C#中,许多集合实现了.toArray()方法,对于那些没有的,可以使用lambda表达式来检索数组。
编辑:
看到你对"No Refunds No Returns"答案的评论。如果你正在返回一个字典,那么数组可能不适用于你。在这种情况下,我建议返回一个接口而不是具体的实现。
在C#中(例如):
public IDictionary<string, object> MyMethod()
{
    Dictionary<string, object> myDictionary = new Dictionary<string, object>();

    // do stuff here

    return myDictionary;
}

编辑2

您可能需要实现自己的只读字典类,并在必要的方法中抛出异常以防止添加等操作。

在C#中(例如)(不是完整的解决方案):

public class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    private IDictionary<TKey, TValue> _innerDictionary;

    public ReadOnlyDictionary(IDictionary<TKey, TValue> innerDictionary)
    {
        this._innerDictionary = innerDictionary;
    }

    public void Add(TKey key, TValue value)
    {
        throw new NotImplementedException();
    }

    public bool Remove(TKey key)
    {
        throw new NotImplementedException();
    }

    public void Add(KeyValuePair<TKey, TValue> item)
    {
        throw new NotImplementedException();
    }

    public void Clear()
    {
        throw new NotImplementedException();
    }

    public bool IsReadOnly
    {
        get { return true; }
    }

    public bool Remove(KeyValuePair<TKey, TValue> item)
    {
        throw new NotImplementedException();
    }

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return _innerDictionary.GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _innerDictionary.GetEnumerator();
    }
}

将字典作为“IDictionary”返回在原则上是正确的,但该特定接口仍具有可变方法。 - Erik Forbes

0

我从未听说过这个建议。如果你做得不好可能会存在线程安全问题,但如果需要,你可以解决这个问题。


0

也许混淆出现在只读集合(即不可变集合)上?如果是这样,Eric Lippert的一系列优秀的帖子详细介绍了如何构建这些集合。

引用一下: 如果您知道数据结构永远不会改变,那么对数据结构进行推理就容易得多。由于它们无法修改,因此它们自动是线程安全的。由于它们无法修改,因此您可以维护过去结构的堆栈“快照”,并且突然撤消-重做实现变得微不足道。


0
返回一个 IEnumerable<T> 怎么样?这样调用者就可以通过 Linq 轻松地过滤结果,而不会改变原始结构。
显然,对于字典,这将是 IEnumerable<KeyValuePair<T,U>>
编辑:对于查找,您可能想要使用 ToLookup() 扩展和 ILookup。

这可能起作用,但是我想保留字典提供的查找语义。虽然字典在技术上是可枚举的,但枚举字典并不是我追求的目的。 - Erik Forbes
我考虑了Lookup类型 - 但它似乎是从TKeyIEnumerable<TValue>的查找 - 这完全不是我想要的。 =\ - Erik Forbes

0
我听到的不允许这种行为的原因是,一个集合或字典类型旨在被客户端使用(但不修改),却暴露了太多的行为,给调用者一种错觉,即调用者可以修改该类型。
但这不是幻觉。调用者确实可以修改该类型(好吧,是该类型的实例)。这到底是什么问题?
按照同样的逻辑,DataTable.Select() 不应该返回 DataRow[],因为调用者不仅可以操作该数组的成员资格,还可以更改基础数据!
而且,返回一个类似于不可变字典的类,它具有 ToDictionary() 方法的想法:从中获得什么好处呢?
的确,返回不可变对象使您能够在不更改 API 的情况下实现内部化。但这是我能想到的唯一优点。

0
使用可变类对象传递数据的主要问题在于,每个可变对象都包含两种主要状态:
  1. 所有字段及其引用的对象的内容。
  2. 存在于其中的所有引用集合以及可能对这些引用进行的操作。
如果一个方法接受一个可变对象(无论是集合还是其他东西)作为参数,并且它的契约规定它将以某种方式改变它(例如向集合添加项目),但不保留任何对它的引用,则调用者将知道在方法调用后存在于该对象的引用集合与调用之前相同。如果调用者从未将对象暴露给外部世界,只是将其传递给这样的方法,那么跟踪存在的引用将很容易。

另一方面,如果一个方法向调用者返回一个可变对象,跟踪传入和传出的对象可能是困难或不可能的,除非每个调用者接收到一个不同的可变对象。在每次调用时让被调用的函数创建一个新的可变对象,并根据需要填充该对象的数据,这当然是可行的方法,但通常最好让调用者创建新的对象。这样,调用者可以根据需要回收对象(提高性能),并且清楚地了解正在发生的情况。例如,如果Customer是一个可变类,那么执行以下操作:

  Customer myCustomer = Database.GetCustomer("Fred Smith");

不清楚对myCustomer进行更改是否会影响数据库。相比之下,如果代码改为:

  Customer myCustomer = new Customer;
  Database.LoadCustomer(myCustomer, "Fred Smith");

则更清楚地表明myCustomer中的数据未附加到数据库(或其他任何内容)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接