检测IEnumerable“状态机”


17

我刚刚读了一篇有趣的文章,《用C#收益回报变得太可爱了》

这让我想知道最好的方法是检测IEnumerable是否是实际的可枚举集合,或者它是使用yield关键字生成的状态机。

例如,您可以将DoubleXValue(来自本文)修改为以下内容:

private void DoubleXValue(IEnumerable<Point> points)
{
    if(points is List<Point>)
      foreach (var point in points)
        point.X *= 2;
    else throw YouCantDoThatException();
}

问题1)有更好的方法吗?

问题2)创建API时,我应该担心这一点吗?


1
您应该使用接口而不是具体的接口,并且使用较低的接口,ICollection<T>这将是更好的选择,因为并非所有集合都是这样List<T>。例如,数组Point[]实现IList<T>但不是List<T>
Trevor Pilley

3
欢迎使用可变类型的问题。Ienumerable被隐式地假定为不可变的数据类型,因此变异实现违反了LSP。本文是对副作用危险的完美解释,因此请练习编写C#类型,以尽可能避免出现副作用,而您不必为此担心。
Jimmy Hoffa 2013年

1
@JimmyHoffa IEnumerable是不可变的,因为您不能在枚举时修改集合(添加/删除),但是,如果列表中的对象是可变的,则在枚举列表时修改它们是完全合理的。
Trevor Pilley

@TrevorPilley我不同意。如果期望该集合是不可变的,则消费者的期望是成员不会被外部参与者突变,这被称为副作用,并且违反了不可变性期望。违反POLA,并隐式包含LSP。
Jimmy Hoffa 2013年

使用ToList怎么样?msdn.microsoft.com/zh-cn/library/bb342261.aspx它不会检测它是否是由yield生成的,而是使其变得无关紧要。
luiscubal

Answers:


22

据我了解,您的问题似乎基于不正确的前提。让我看看我是否可以重构推理:

  • 链接到的文章介绍了自动生成的序列如何表现出“惰性”行为,并说明了这如何导致违反直觉的结果。
  • 因此,我可以通过检查是否自动生成来确定给定的IEnumerable实例是否将表现出这种惰性行为。
  • 我怎么做?

问题在于第二个前提是错误的。即使您可以检测到给定的IEnumerable是否是迭代器块转换的结果(是的,也有这样做的方法),但这也无济于事,因为假设是错误的。让我们说明一下原因。

class M { public int P { get; set; } }
class C
{
  public static IEnumerable<M> S1()
  {
    for (int i = 0; i < 3; ++i) 
      yield return new M { P = i };
  }

  private static M[] ems = new M[] 
  { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  public static IEnumerable<M> S2()
  {
    for (int i = 0; i < 3; ++i)
      yield return ems[i];
  }

  public static IEnumerable<M> S3()
  {
    return new M[] 
    { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  }

  private class X : IEnumerable<M>
  {
    public IEnumerator<X> GetEnumerator()
    {
      return new XEnum();
    }
    // Omitted: non generic version
    private class XEnum : IEnumerator<X>
    {
      int i = 0;
      M current;
      public bool MoveNext()
      {
        current = new M() { P = i; }
        i += 1;
        return true;
      }
      public M Current { get { return current; } }
      // Omitted: other stuff.
    }
  }

  public static IEnumerable<M> S4()
  {
    return new X();
  }

  public static void Add100(IEnumerable<M> items)
  {
    foreach(M item in items) item.P += 100;
  }
}

好吧,我们有四种方法。S1和S2是自动生成的序列;S3和S4是手动生成的序列。现在假设我们有:

var items = C.Sn(); // S1, S2, S3, S4
S.Add100(items);
Console.WriteLine(items.First().P);

S1和S4的结果为0;每次枚举序列时,都会获得对创建的M的新引用。S2和S3的结果为100;每次枚举序列时,都会得到与上次获得的M相同的引用。序列代码是否自动生成与所枚举的对象是否具有参照身份的问题正交。这两个属性-自动生成和引用身份-实际上彼此无关。您链接到的文章对其进行了一些补充。

除非序列提供者被记录为总是提供具有引用身份的对象,否则假设这样做是不明智的。


2
我们都知道您将在这里得到正确的答案,这是给定的;但是,如果您可以在术语“引用身份”中添加更多定义,那可能很好,我将其理解为“不可变的”,但这是因为我将C#中的不可变与每个get或value类型返回的新对象进行了混合,我认为与您所指的是同一件事。尽管可以通过多种其他方式获得不变性,但这是C#中唯一合理的方式(据我所知,您可能很清楚我不知道的方式)。
Jimmy Hoffa 2013年

2
@JimmyHoffa:如果Object.ReferenceEquals(x,y)返回true,则两个对象引用x和y具有“引用身份”。
埃里克·利珀特

总是很高兴直接从源头获得响应。谢谢埃里克。
ConditionRacer

10

我认为关键概念是您应将任何IEnumerable<T>集合视为不可变的:不应从中修改对象,而应使用新对象创建新集合:

private IEnumerable<Point> DoubleXValue(IEnumerable<Point> points)
{
    foreach (var point in points)
        yield return new Point(point.X * 2, point.Y);
}

(或者使用LINQ更加简洁地编写同样的内容Select()。)

如果您实际上要修改集合中的项目,请不要使用IEnumerable<T>IList<T>或者使用(或者IReadOnlyList<T>如果您不想修改集合本身,也可以只修改其中的项目,并且使用的是.Net 4.5):

private void DoubleXValue(IList<Point> points)
{
    foreach (var point in points)
        point.X *= 2;
}

这样,如果有人尝试将您的方法与一起使用IEnumerable<T>,它将在编译时失败。


1
真正的钥匙就像您说的那样,当您要更改某些内容时,返回带有修改的新的枚举。作为一种类型,无数是完全可行的,没有理由要更改,只是使用它的技术需要像您所说的那样被理解。+1表示如何使用不可变类型。
吉米·霍法

1
有点相关-您还应该将每个IEnumerable视为通过延迟执行来完成。当您实际枚举IEnumerable引用时,可能不正确。例如,在锁内返回LINQ语句可能会导致一些有趣的意外结果。
丹·里昂斯

@JimmyHoffa:我同意。我更进一步,尝试避免重复IEnumerable多次。可以通过调用ToList()和遍历列表来消除重复执行此操作的需要,尽管在OP所示的特定情况下,我更喜欢svick的具有DoubleXValue的解决方案,该解决方案还返回IEnumerable。当然,在实际代码中,我可能会使用它LINQ来生成这种类型的IEnumerable。但是,yield在OP的问题中,svick的选择是有道理的。
布莱恩

@Brian是的,我不想过多更改代码以说明我的意思。在实际代码中,我会使用Select()。关于IEnumerable:ReSharper的多次迭代对此很有帮助,因为这样做可以警告您。
svick

5

我个人认为该文章中强调的问题是由于IEnumerable这些天来许多C#开发人员过度使用了接口。当您需要对象集合时,它似乎已成为默认类型-但是,有很多信息IEnumerable根本无法告诉您……关键的是:它是否已被修正*。

如果发现需要检测an IEnumerable确实是an IEnumerable还是其他,则说明抽象泄漏了,并且您可能处于参数类型过于松散的情况。如果您需要IEnumerable单独提供的额外信息,请通过选择其他接口之一(例如ICollection或)来明确要求IList

编辑下降投票者 请返回并仔细阅读问题。OP正在寻求一种方法,以确保IEnumerable实例在传递给他的API方法之前已经实现。我的回答解决了这个问题。关于正确的使用方式,还有一个更广泛的问题IEnumerable,当然,对OP粘贴的代码的期望是基于对该较广泛问题的误解,但是OP并未询问如何正确使用IEnumerable或如何使用不可变类型。不赞成我没有回答没有提出的问题是不公平的。

* 我使用术语reify,其他人使用stabilizematerialize。我认为社区尚未采用通用约定。


2
ICollection和的问题IList是它们是可变的(或至少看起来是这样)。IReadOnlyCollectionIReadOnlyList从.NET 4.5解决一些这些问题。
svick

请解释下票?
MattDavey

1
我不同意您应该更改所使用的类型。无数是一个很好的类型,它通常可以具有不变性的好处。我认为真正的问题是人们不了解如何在C#中使用不可变类型。毫不奇怪,这是一种非常有状态的语言,因此对于许多C#开发人员来说这是一个新概念。
Jimmy Hoffa 2013年

只有ienumerable允许延迟执行,因此将其作为参数禁用会删除C#的全部功能
Jimmy Hoffa,2013年

2
我不赞成,因为我认为这是错误的。我认为这是SE的精神,要确保人们不会在错误的信息中存货,我们所有人都应否决那些答案。也许我错了,很可能我错了,而你是正确的。这取决于广大社区的投票决定。抱歉,您个人认为,如果有什么安慰,我已经写了很多否定表决的答案,并已将其删除,因为我接受社区认为这是错误的,所以我不太可能是正确的。
Jimmy Hoffa 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.