6

有一些与此类似的问题涉及正确的输入和输出类型。我的问题是什么好的做法、方法命名、选择参数类型或类似的东西可以防止延迟执行事故?

这些是最普遍的,IEnumerable这是一种非常常见的参数类型,因为:

  • 遵循稳健性原则 “在你所做的事情上保守,在你接受别人的事情上自由”

  • 广泛用于Linq

  • IEnumerable在集合层次结构中处于较高位置并且早于较新的集合类型

但是,它也引入了延迟执行。现在我们可能在设计方法(尤其是扩展方法)时出错了,因为我们认为最好的想法是采用最基本的类型。所以我们的方法看起来像:

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> lstObject)
{
    foreach (T t in lstObject)
       //some fisher-yates may be
}

危险显然是当我们将上述函数与惰性混合在一起时Linq,它非常容易受到影响。

var query = foos.Select(p => p).Where(p => p).OrderBy(p => p); //doesn't execute
//but
var query = foos.Select(p => p).Where(p => p).Shuffle().OrderBy(p => p);
//the second line executes up to a point.

更大的编辑:

重新打开这个:对语言功能的批评没有建设性 - 但是要求良好的实践是 StackOverflow 的亮点。更新了问题以反映这一点。

这里有一个很大的编辑:

澄清上述行 - 我的问题不是关于第二个表达式没有得到评估,严重的是没有。程序员都知道。我担心的是Shuffle到目前为止实际执行查询的方法。查看第一个查询,没有执行任何操作。现在类似地,在构造另一个 Linq 表达式(应该稍后执行)时,我们的自定义函数正在玩破坏游戏。换句话说,如何让调用者知道Shuffle并不是他们在 Linq 表达式的那个点想要的那种功能。我希望重点是回家。道歉!:)虽然它就像检查方法一样简单,但我在问你们通常如何进行防御性编程..

上面的例子可能没有那么危险,但你明白了。那是某些(自定义)函数不适合Linq延迟执行的想法。问题不仅与性能有关,还与意想不到的副作用有关。

但是像这样的函数可以通过以下方式发挥作用Linq

public static IEnumerable<S> DistinctBy<S, T>(this IEnumerable<S> source, 
                                              Func<S, T> keySelector)
{
    HashSet<T> seenKeys = new HashSet<T>(); //credits Jon Skeet
    foreach (var element in source)
        if (seenKeys.Add(keySelector(element)))
            yield return element;
}

如您所见,这两个函数都 take IEnumerable<>,但调用者不知道这些函数是如何反应的。那么你们在这里采取的一般警告措施是什么?

  1. 适当地命名我们的自定义方法,以便它为调用者提供一个好兆头或不好的想法Linq

  2. 惰性方法移动到不同的名称空间,并将Linq-ish 保留到另一个名称空间,以便它至少给出某种想法?

  3. 不接受IEnumerableas 参数来immediately执行方法,而是采用更派生的类型或具体类型本身,从而IEnumerable单独留给惰性方法?这给调用者增加了执行可能未执行的表达式的负担?这对我们来说是很有可能的,因为外部Linq世界我们几乎不处理IEnumerables,而且大多数基本的集合类ICollection都至少实现了。

还是别的什么?我特别喜欢第三个选项,这就是我想要的,但我想在此之前得到你的想法。即使是优秀的程序员,我也看到了很多代码(有点像扩展方法!) ,他们在方法Linq中接受IEnumerable并对其执行或类似的操作。ToList()我不知道他们如何应对副作用。

编辑:在投反对票和回答之后,我想澄清一下,这不是程序员不知道 Linq 是如何工作的(我们的熟练程度可能在某种程度上,但那是另一回事),而是很多函数都是写的那时linq算进去了。现在将立即执行的方法与 Linq 扩展方法链接起来很危险。所以我的问题是,程序员是否遵循一般准则,让调用者知道从 Linq 端使用什么,不使用什么?它更多的是关于防御性编程,而不是如果你不知道使用它,那么我们就无能为力!(或者至少我相信)..

4

3 回答 3

7

如您所见,这两个函数都 take IEnumerable<>,但调用者不知道这些函数是如何反应的。

这只是一个文档问题。查看DistinctByMoreLINQ中的文档,其中包括:

尽管保留了一组已经看到的键,但该运算符使用延迟执行并流式传输结果。如果多次看到一个键,则只返回具有该键的第一个元素。

是的,在使用之前了解成员的用途很重要,对于接受/返回任何类型的集合的事情,有很多重要的事情需要了解:

  • 集合会立即阅读,还是推迟阅读?
  • 返回结果时是否会流式传输集合?
  • 如果接受的声明集合类型是可变的,该方法会尝试对其进行变异吗?
  • 如果返回的声明的集合类型是可变的,它实际上是一个可变的实现吗?
  • 返回的集合是否会被其他操作更改(例如,它是一个可以在类中修改的集合的只读视图)
  • null可接受的输入值吗?
  • null可接受的元素值吗?
  • 该方法会返回null吗?

所有这些都值得考虑——其中大部分在 LINQ 之前很久就值得考虑。

真正的寓意是,“在你调用它之前,确保你知道它的行为方式。” 在 LINQ 之前确实如此,而 LINQ 并没有改变它。它只是介绍了以前很少出现的两种可能性(延迟执行和流式传输结果)。

于 2012-11-26T06:57:43.240 回答
1

在任何有意义的地方使用 IEnumerable ,并进行防御性编码

正如 SLaks 在评论中指出的那样,延迟执行IEnumerable从一开始就成为可能,并且自从 C# 2.0 引入了该yield语句以来,自己实现延迟执行非常容易。例如,此方法返回一个使用延迟执行返回一些随机数的 IEnumerable:

public static IEnumerable<int> RandomSequence(int length)
{
    Random rng = new Random();
    for (int i = 0; i < length; i++) {
        Console.WriteLine("deferred execution!");
        yield return rng.Next();
    }
}

因此,每当您使用foreach循环遍历 IEnumerable 时,您必须假设在迭代之间可能发生任何事情。它甚至可能引发异常,因此您可能希望将 foreach 循环放在try/finally.

如果调用者传入一个 IEnumerable,它做了一些危险的事情或永远不会停止返回数字(无限序列),这不是你的错。您不必检测它并抛出错误;只需添加足够的异常处理程序,以便您的方法可以在出现问题时自行清理。对于像这样简单的事情Shuffle,没有什么可做的;让调用者处理异常。

在极少数情况下,您的方法确实无法处理无限序列,请考虑接受不同的类型,例如IList. 但即使IList 也无法保护您免受延迟执行的影响——您不知道哪个类正在实现 IList,也不知道它为每个元素做了什么伏都教!在非常罕见的情况下,您在迭代时确实不允许任何意外代码运行,您应该接受一个数组,而不是任何类型的接口。

于 2012-11-26T00:13:02.623 回答
0

延迟执行与类型无关。如果您以这种方式编写代码,则任何使用迭代器的 linq 方法都有可能延迟执行。Select(), Where(),OrderByDescending()例如所有使用迭代器并因此延迟执行。是的,这些方法需要一个IEnumerable<T>,但这并不意味着这IEnumerable<T>是问题所在。

那是某些(自定义)函数不适合 Linq 的延迟执行理念。问题不仅与性能有关,还与意想不到的副作用有关。

那么你们在这里采取的一般警告措施是什么?

没有任何。老实说,我们IEnumerable 到处都在使用,不存在人们不了解“副作用”的问题。“延迟执行的 Linq 理念”是其在 Linq-to-SQL 之类的用途中的核心。在我看来,自定义功能的设计并没有想象中那么清晰。如果人们正在编写代码以使用 LINQ 并且他们不了解它在做什么,那么这就是问题所在,而不是IEnumerable恰好是基类型的事实。

您所有的想法都只是对这样一个事实的包装,即听起来您的程序员只是不了解 linq 查询。如果您不需要延迟执行(听起来好像不需要),那么只需在函数退出之前强制所有内容进行评估。对您的结果调用 ToList() 并在消费者希望使用的一致 API 中返回它们 - 列表、数组、集合或 IEnumerables。

于 2012-11-26T00:07:37.457 回答