IEnumerable <t>的实现适用于foreach但不适用于LINQ </t>

时间:2010-01-25 16:53:55

标签: c# generics ienumerable

我目前使用以下基本集合。

  abstract class BaseCollection : Collection<BaseRecord>
  {       
  }

我想用下面的通用集合替换它。然而,由于当前实现BaseCollection的派生类的数量,这不是一项微不足道的任务。

  abstract class BaseCollection<TRecord> : Collection<TRecord> where TRecord : BaseRecord,new()
  {

  }

我不想进行大规模的改革,而是要慢慢推出这个新系列。

   abstract class BaseCollection<TRecord> : BaseCollection,IEnumerable<TRecord> where TRecord : BaseRecord,new()
   {
      public new IEnumerator<TRecord> GetEnumerator()
      {
         return Items.Cast<TRecord>().GetEnumerator(); 
      }
   }

虽然我可以使用foreach枚举集合,但下面的LINQ语句不能编译。这可能吗?我意识到这有点黑客但我不确定如何完成这项工作。

   class Program
   {
      static void Main(string[] args)
      {
         DerivedCollection derivedCollection = new DerivedCollection
         {
            new DerivedRecord(),
            new DerivedRecord()
         };
         foreach (var record in derivedCollection)
         {
            record.DerivedProperty = "";
         }
         var records = derivedCollection.Where(d => d.DerivedProperty == "");
      }
   }

以下是上面使用的两条记录。谢谢。

   class DerivedRecord : BaseRecord
   {
      public string DerivedProperty { get; set; }
   }

   abstract class BaseRecord
   {
      public string BaseProperty { get; set; }
   }

这是派生的集合。

   class DerivedCollection : BaseCollection<DerivedRecord>
   {

   }

3 个答案:

答案 0 :(得分:4)

Foreach使用一些“魔法”来确定要循环的类型。它不需要集合支持IEnumerable任何东西。这可能解释了不同之处。

问题是编译器无法找出Where的泛型类型参数。如果你这样做,它将起作用:

IEnumerable<DerivedRecord> ie = derivedCollection;
ie.Where(d => d.DerivedProperty == "");

没有使用强制转型,但可用选项的集合已减少到一个。

或者您可以明确指定类型参数:

derivedCollection.Where<DerivedRecord>(d => d.DerivedProperty == "");

我认为要解决您的问题,您必须阻止BaseCollection继承其他版本的IEnumerable<T>

abstract class BaseCollection
{
    private readonly Collection<BaseRecord> _realCollection = new Collection<BaseRecord>();

    public void Add(BaseRecord rec)
    {
        _realCollection.Add(rec);
    }

    public IEnumerable<BaseRecord> Items
    {
        get { return _realCollection; }
    }
}

我仍在实例化Collection<T>但是作为单独的对象而不是基类。然后将内容转发给它,以便在必要时模仿其API。

派生的通用版本需要完全实现IEnumerable<T>

class BaseCollection<TRecord> : BaseCollection, IEnumerable<TRecord> 
                                    where TRecord :  BaseRecord,new()
{
    public IEnumerator<TRecord> GetEnumerator()
    {
        return Items.Cast<TRecord>().GetEnumerator(); 
    }

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

然后,您的示例代码的其余部分在没有我最初发布的变通方法的情况下工作正常,因为它只有一个IEnumerable<T>实现可以使用。

更新,更多详情

这是同样情况下的一个非常精简的版本,打破了IEnumerable的所有链接,从头开始宣布一切,所以我们可以看到发生了什么。

public class R1 { }
public class R2 { }
public interface I<T> { }
public class C1<T> : I<T> { }
public class C2 : C1<R1>, I<R2> { }

class Program
{
    public static I<T> M<T>(I<T> i) { return i; }

    static void Main(string[] args)
    {
        var c2 = new C2();
        var v = M(c2); // Compiler error - no definition for M
    }
}

R1R2就像两个记录类。在您的版本中,R2会继承R1 - 但这与此问题无关,所以我省略了它。 I<T>是通用界面,代表IEnumerable<T>。但我的版本没有方法!它们也与此问题无关。

然后我将你的集合类继承系统折叠成两层。基类C1实现I<T>,然后派生类C2选择要求C2实施I<R1>,然后直接实现I<R2>。层数也没有区别。也没有用where声明类型约束,因此这里也省略了它们。

结果是C2有两个I<T>实现:I<R1>I<R2>。一个是继承自C1,另一个是自我添加。

最后我有一个方法M,它代表Linq中的Where。它不需要是一个显示问题的扩展方法,所以为了清晰起见,我已经使它成为一个普通的静态方法。

因此,当我们调用方法M时,编译器必须弄清楚T是什么。它通过查看我们作为方法的唯一参数传递的内容来实现这一点,该参数必须支持I<T>。不幸的是,我们正在传递支持I<R1>I<R2>的内容,那么类型推断过程如何在它们之间做出选择呢?它不能。

由于我的界面没有方法,明确地将new放在方法前面对我没有帮助,这就是为什么它对你没有帮助。问题不在于决定要调用的接口中的哪个方法,而是将M的参数视为I<R1>还是I<R2>

为什么编译器不将此报告为类型推断问题?根据C#3.0规范,它只是没有 - 首先运行类型推断,产生一组可用的重载,然后运行重载决策以选择最佳选择。如果类型推断无法在泛型方法的两种可能扩展之间做出决定,则它会消除这两种情况,因此重载解析甚至都不会看到它们,因此错误消息表明没有任何适用的方法称为M

(但是如果你有Resharper,它有自己的编译器,它用于在IDE中提供更详细的错误,在这种情况下它具体说明:“方法M的类型参数不能从用法推断”。 )

现在,为什么foreach会有所不同?因为它甚至不安全!它可以追溯到添加仿制药之前。它甚至没有看接口。它只是在它循环的任何类型中查找名为GetEnumerator的公共方法。例如:

public class C
{
    public IEnumerator GetEnumerator() { return null; }
}

就编译器而言,这是一个非常好的集合! (当然它会在运行时爆炸,因为它返回null IEnumerator。)但请注意缺少IEnumerable或任何泛型。这意味着foreach进行隐式演员。

因此,为了将此与您的代码相关联,您需要从BaseRecordDerivedRecord进行“向下投射”,您可以使用Linq的Cast运算符实现。嗯,foreach无论如何都会为你做这件事。在上面的示例中,C实际上是object类型项的集合。但我可以写:

foreach (string item in new C())
{
    Console.WriteLine(item.Length);
}

编译器愉快地将object的静默强制转换插入string。那些物品可以是任何东西......哎呀!

这就是var的出现很棒的原因 - 总是使用var来声明你的foreach循环变量,这样编译器就不会插入强制转换。它将使变量成为它在编译时从集合中推断出的最具体的类型。

答案 1 :(得分:0)

更加丑陋,但有效:

(IEnumerable<DerivedRecord>)derivedCollection).Where(d => d.DerivedProperty == "")

答案 2 :(得分:0)

op可能已经解决了这个问题,但如果它对其他任何人有帮助,那么当你尝试使用BaseCollection时,你现有的Linq就会中断,因为它有两个IEnumerable类型:IEnumerable<TRecord>IEnumerable<BaseRecord>(继承自BaseCollection - &gt; Collection<BaseRecord>)。要在使用两个IEnumerable类型的同时编译现有的Linq,请通过实现IQueryable<TypeYouWantToUse>来定义Linq要使用的IEnumerable。

abstract class BaseCollection<TRecord> : BaseCollection, IEnumerable<TRecord>, IQueryable<TRecord> where TRecord : BaseRecord, new()
{
    public new IEnumerator<TRecord> GetEnumerator()
    {
        return Items.Cast<TRecord>().GetEnumerator();
    }

    #region IQueryable<TRecord> Implementation
    public Type ElementType
    {
        get { return typeof(TRecord); }
    }

    public System.Linq.Expressions.Expression Expression
    {
        get { return this.ToList<TRecord>().AsQueryable().Expression; }
    }

    public IQueryProvider Provider
    {
        get { return this.ToList<TRecord>().AsQueryable().Provider; }
    }
    #endregion
}