【问题标题】:Grafting LINQ onto C# 2 library将 LINQ 移植到 C# 2 库上
【发布时间】:2011-02-05 01:56:15
【问题描述】:

我正在编写一个数据访问层。它将具有 C# 2 和 C# 3 客户端,因此我正在针对 2.0 框架进行编译。尽管鼓励使用存储过程,但我仍在尝试提供相当完整的能力来执行即席查询。我已经把它做得很好了。

为了方便 C# 3 客户端,我尝试尽可能多地提供与 LINQ 查询语法的兼容性。 Jon Skeet noticed LINQ 查询表达式是鸭子类型的,所以我没有有一个 IQueryableIQueryProvider(或 IEnumerable<T>)来使用它们。我只需要提供具有正确签名的方法。

所以我得到了SelectWhereOrderByOrderByDescendingThenByThenByDescending 工作。我需要帮助的地方是JoinGroupJoin。我已经让他们工作了,但只有一次加入。

我所拥有的一个简短的可编译示例是这样的:

// .NET 2.0 doesn't define the Func<...> delegates, so let's define some workalikes
delegate TResult FakeFunc<T, TResult>(T arg);
delegate TResult FakeFunc<T1, T2, TResult>(T1 arg1, T2 arg2);

abstract class Projection{
    public static Condition operator==(Projection a, Projection b){
        return new EqualsCondition(a, b);
    }
    public static Condition operator!=(Projection a, Projection b){
        throw new NotImplementedException();
    }
}
class ColumnProjection : Projection{
    readonly Table  table;
    readonly string columnName;

    public ColumnProjection(Table table, string columnName){
        this.table      = table;
        this.columnName = columnName;
    }
}
abstract class Condition{}
class EqualsCondition : Condition{
    readonly Projection a;
    readonly Projection b;

    public EqualsCondition(Projection a, Projection b){
        this.a = a;
        this.b = b;
    }
}
class TableView{
    readonly Table        table;
    readonly Projection[] projections;

    public TableView(Table table, Projection[] projections){
        this.table       = table;
        this.projections = projections;
    }
}
class Table{
    public Projection this[string columnName]{
        get{return new ColumnProjection(this, columnName);}
    }

    public TableView Select(params Projection[] projections){
        return new TableView(this, projections);
    }
    public TableView Select(FakeFunc<Table, Projection[]> projections){
        return new TableView(this, projections(this));
    }
    public Table     Join(Table other, Condition condition){
        return new JoinedTable(this, other, condition);
    }
    public TableView Join(Table inner,
                          FakeFunc<Table, Projection> outerKeySelector,
                          FakeFunc<Table, Projection> innerKeySelector,
                          FakeFunc<Table, Table, Projection[]> resultSelector){
        Table join = new JoinedTable(this, inner,
            new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
        return join.Select(resultSelector(this, inner));
    }
}
class JoinedTable : Table{
    readonly Table     left;
    readonly Table     right;
    readonly Condition condition;

    public JoinedTable(Table left, Table right, Condition condition){
        this.left      = left;
        this.right     = right;
        this.condition = condition;
    }
}

这让我可以在 C# 2 中使用相当不错的语法:

Table table1 = new Table();
Table table2 = new Table();

TableView result =
    table1
    .Join(table2, table1["ID"] == table2["ID"])
    .Select(table1["ID"], table2["Description"]);

但 C# 3 中的语法更好:

TableView result =
    from t1 in table1
    join t2 in table2 on t1["ID"] equals t2["ID"]
    select new[]{t1["ID"], t2["Description"]};

这很好用,并且给了我与第一种情况相同的结果。问题是如果我想加入第三张桌子。

TableView result =
    from t1 in table1
    join t2 in table2 on t1["ID"] equals t2["ID"]
    join t3 in table3 on t1["ID"] equals t3["ID"]
    select new[]{t1["ID"], t2["Description"], t3["Foo"]};

现在我收到一个错误(无法将类型“AnonymousType#1”隐式转换为“Projection[]”),可能是因为第二个连接试图将第三个表连接到包含前两个表的匿名类型。当然,这种匿名类型没有Join 方法。

关于如何做到这一点的任何提示?

【问题讨论】:

    标签: c# linq data-access-layer


    【解决方案1】:

    这是一个非常有趣的设计,我喜欢它! 正如您所说,问题在于您对 Join 方法的定义过于具体。您的定义与 LINQ 中的定义之间的主要区别如下:

    public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
        /* cut */, Func<TOuter, TInner, TResult> resultSelector)
    
    public TableView Join(
         /* cut */, FakeFunc<Table, Table, Projection[]> resultSelector)
    

    当 LINQ 编译带有多个 join 子句的查询时,它会按顺序调用它们并为第一个子句自动生成 resultSelector - 并且生成的代码返回一个简单的匿名类型,其中包含来自两个源的元素表。因此,如果我是正确的,在您的情况下,生成的匿名类型将如下所示:

    new { t1 : Projection; t2 : Projection }
    

    不幸的是,这与Projection[] 不兼容(尽管语义,差别不大)。恐怕解决这个问题的唯一方法是使用动态类型转换和反射。

    • 您需要修改Join,使其具有在resultSelector 中使用的泛型类型参数TResult

    • Join 方法中,您将运行TResult res = resultSelector(...),然后您需要对res 值执行一些操作。

    • 如果res is Projection[],那么您可以使用现有代码(这种情况将用于包含单个join 子句的查询)

    • 在另一种情况下,res 是与上述类似的匿名类型。这意味着您需要使用反射来获取该类型的属性值并将它们转换为 Projection 值的数组(然后像现在一样做同样的事情)。

    我没有尝试实现它,但我认为它可能会起作用......

    【讨论】:

    • 谢谢,您为我指明了正确的道路。考虑到您的建议,我开始使用 IEnumerables 模拟事物并在 Reflector 中四处寻找。经过足够的戳,我想出了我需要做什么。好消息:不需要运行时反射。不过,它需要一个额外的类。我将在另一个答案中发布完整的详细信息。不过,您会得到接受的答案和 +1。
    • @P Daddy:我很高兴我的回答有所帮助 - 我没有意识到您可以通过添加一个带数组的重载来从匿名类型中恢复 - 这是一个不错的技巧!
    【解决方案2】:

    以下内容有点长。如果您只对让它工作感兴趣,而不关心为什么或如何工作,那么请跳到最后两个代码部分。


    Tomas Petricek 的answer 是正确的方向,但只走了一半。 resultSelectorTResult 确实需要是通用的。连接确实是链式的,中间结果包含一个匿名类型,由每个连接的左右部分组成(分别称为外部和内部)。

    让我们看看我之前的查询:

    TableView result =
        from t1 in table1
        join t2 in table2 on t1["ID"] equals t2["ID"]
        join t3 in table3 on t1["ID"] equals t3["ID"]
        select new[]{t1["ID"], t2["Description"], t3["Foo"]};
    

    这被翻译成这样的:

    var intermediate =
        table1.Join(
            table2, t1=>t1["ID"], t2=>t2["ID"],
            (t1, t2)=>new{t1=t1, t2=t2}
        );
    TableView result =
        intermediate.Join(
            table3, anon=>anon.t1["ID"], t3=>t3["ID"],
            (anon, t3)=>new[]{anon.t1["ID"], anon.t2["ID"], t3["Foo"]}
        );
    

    添加额外的连接使模式更清晰:

    TableView result =
        from t1 in table1
        join t2 in table2 on t1["ID"] equals t2["ID"]
        join t3 in table3 on t1["ID"] equals t3["ID"]
        join t4 in table4 on t1["ID"] equals t4["ID"]
        select new[]{t1["ID"], t2["Description"], t3["Foo"], t4["Bar"]};
    

    这大致翻译为:

    var intermediate1 =
        table1.Join(
            table2, t1=>t1["ID"], t2=>t2["ID"],
            (t1, t2)=>new{t1=t1, t2=t2}
        );
    var intermediate2 =
        intermediate1.Join(
            table3, anon1=>anon1.t1["ID"], t3=>t3["ID"],
            (anon1, t3)=>new{anon1=anon1, t3=t3}
        );                 
    TableView result =
        intermediate2.Join(
            table4, anon2=>anon2.anon1.t1["ID"], t4=>t4["ID"],
            (anon2, t3)=>new[]{
                anon2.anon1.t1["ID"], anon2.anon1.t2["ID"],
                anon2.t3["Foo"], t4["Bar"]
            }
        );
    

    所以resultSelector 的返回值将是两个不同的东西。对于最终的连接,结果是选择列表,这就是我已经在处理的情况。对于每个其他连接,它将是一个包含连接表的匿名类型,根据我在查询中为它们分配的别名给它们命名。 LINQ 显然会处理匿名类型中的间接性,并根据需要逐步执行。

    很明显,我需要的不是一个,而是两个Join 方法。我为最终连接工作得很好,我需要为中间连接添加另一个。请记住,我已经拥有的方法返回一个TableView

    public TableView Join(Table inner,
                          FakeFunc<Table, Projection> outerKeySelector,
                          FakeFunc<Table, Projection> innerKeySelector,
                          FakeFunc<Table, Table, Projection[]> resultSelector){
        Table join = new JoinedTable(this, inner,
            new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
        return join.Select(resultSelector(this, inner));
    }
    

    现在我需要添加一个通过Join 方法返回的内容,以便可以在链中调用它:

    public Table Join<T>(Table inner,
                         FakeFunc<Table, Projection> otherKeySelector,
                         FakeFunc<Table, Projection> innerKeySelector,
                         FakeFunc<Table, Table, T> resultSelector){
        Table join = new JoinedTable(this, inner,
            new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
        // calling resultSelector(this, inner) would give me the anonymous type,
        // but what would I do with it?
        return join;
    }
    

    添加此方法使我的联接几乎可以工作。我终于可以加入三个或更多的表,但我失去了我的别名:

    TableView result =
        from t1 in table1
        join t2 in table2 on t1["ID"] equals t2["ID"]
        join t3 in table3 on t1["ID"] equals t3["ID"]
                           // ^  error, 't1' isn't a member of 'Table'
        select new[]{t1["ID"], t2["Description"], t3["Foo"]};
                   // ^         ^  error, 't1' & 't2' aren't members of 'Table'
    

    我差点停在这里。毕竟,我可以通过放弃这些丢失的别名来解决它:

    TableView result =
        from t1 in table1
        join t2 in table2 on t1["ID"] equals t2["ID"]
        join t3 in table3 on table1["ID"] equals t3["ID"]
        select new[]{table1["ID"], table2["Description"], t3["Foo"]};
    

    这编译、运行并产生了预期的结果。呜呼!成功,有点。但是,丢失别名并不理想。在实际查询中,表可能更复杂:

    TableView result =
        from t1 in table1
        join t2 in (
            from t in table2
            where t["Amount"] > 20
            select new[]{t["ID"], t["Description"]
        ).AsSubQuery() on t1["ID"] equals t2["ID"]
        join t3 in table3 on t1["ID"] equals t3["ID"]
        select new[]{table1["ID"], t2["Description"], t3["Foo"]};
                                 // ^ error, 't2' isn't a member of 'Table'
    

    在这里,我不能没有 t2 的别名(嗯,我可以,但这会涉及将子查询移出到在主查询之前声明的另一个变量中,但我正在努力提高流畅度,在这里)。

    在看到“'t1' is not a member of 'Table'”消息足够多次后,我终于意识到秘密就在outerKeySelectorJoin 的参数中。 LINQ 只是在寻找一个名为 t1(或其他)的属性,它是该 lambda 参数的成员。我的outerKeySelector 参数都是这样声明的:

    FakeFunc<Table, Projection> outerKeySelector
    

    Table 类当然没有名为t1 的属性或任何其他别名。我怎么能添加这个属性?如果我使用 C# 4,我可能会使用 dynamic 来执行此操作,但如果我使用 C# 4,那么整个设计将有所不同(我确实计划稍后在 C# 4 中重做此操作,用于 . NET 4.0 客户端,充分利用动态类型来提供列投影作为表的实际属性)。但是,在 .NET 2.0 中,我没有动态类型。那么如何创建一个将表别名作为属性的类型呢?

    等一下。拿着电话。 resultSelector 已经给我回了一个!不知何故,我需要抓住这个对象并将它传递给下一个连接中的outerKeySelector。但是怎么做?我不能只将它存储在我的 JoinedTable 类中,因为该类不知道如何转换它。

    就在那时它击中了我。我需要一个通用的中间类来保存这些中间连接结果。它将存储对描述实际连接的JoinedTable 实例的引用,以及对包含别名的此匿名类型的引用。尤里卡!

    代码的最终全功能版本添加了这个类:

    class IntermediateJoin<T>{
        readonly JoinedTable table;
        readonly T           aliases;
    
        public IntermediateJoin(JoinedTable table, T aliases){
            this.table   = table;
            this.aliases = aliases;
        }
    
        public TableView Join(Table inner,
                              FakeFunc<T, Projection> outerKeySelector,
                              FakeFunc<Table, Projection> innerKeySelector,
                              FakeFunc<T, Table, Projection[]> resultSelector){
            var join = new JoinedTable(table, inner,
                new EqualsCondition(outerKeySelector(aliases), innerKeySelector(inner)));
            return join.Select(resultSelector(aliases, inner));
        }
        public IntermediateJoin<U> Join<U>(Table inner,
                                           FakeFunc<T, Projection> outerKeySelector,
                                           FakeFunc<Table, Projection> innerKeySelector,
                                           FakeFunc<T, Table, U> resultSelector){
            var join = new JoinedTable(table, inner,
                new EqualsCondition(outerKeySelector(aliases), innerKeySelector(inner)));
            var newAliases = resultSelector(aliases, inner);
            return new IntermediateJoin<U>(join, newAliases);
        }
    }
    

    这个方法到Table类:

    public IntermediateJoin<T> Join<T>(Table inner,
                          FakeFunc<Table, Projection> outerKeySelector,
                          FakeFunc<Table, Projection> innerKeySelector,
                          FakeFunc<Table, Table, T> resultSelector){
        var join = new JoinedTable(this, inner,
            new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
        var x = resultSelector(this, inner);
        return new IntermediateJoin<T>(join, x);
    }
    

    这提供了功能齐全的连接语法!*

    再次感谢 Tomas Petricek 花时间阅读和理解我的问题,并给予我深思熟虑的回答。

    *GroupJoinSelectMany 还在后面,但我想我现在已经足够了解这些秘密了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-09
      • 1970-01-01
      • 2012-12-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多