对象数组上的Linq外部联接

本文关键字:外部 Linq 数组 对象 | 更新日期: 2023-09-27 18:28:48

考虑一组6个[StringKey,Value]数组,使用伪代码:

object[,2][6] KeyValueArrays; //illustr as array, could also be List<> or Dict<>

我想把它转换成一个单独的表格:

object[,7] KeyWithUpTo6Values  //outer join of the 6 orig key-value sets

如果给定的键出现在6个原始[key,Value]数组中的3个数组中,则该键的连接行将包含3个值和3个null。

问题:我可以用LINQ只使用简单的容器(如数组和泛型列表)来完成这项工作吗;字典?

对象数组上的Linq外部联接

我想我可能遗漏了一些东西,但事实上,你的问题提到了泛型列表&字典,我认为当你说数组时,你指的是某种向量数据结构之王。

因此,可以使用Dictionary<T1,T2>来存储键值对。例如,假设您的键是一个string和一个具有integer类型单个属性的值类MyValueClass。您的数据声明如下所示:

class Program
{
    class MyValueClass
    {
        public int Value {get;set;}
    }
    // Other elements elided for clarity
    private Dictionary<string, MyValueClass> data = new Dictionary<string, MyValueClass>();
}

现在,你说你有N个这样的结构,你想对它们进行外部连接

private Dictionary<string, MyValueClass>[] data = new Dictionary<string, MyValueClass>[6]();

这里的问题是,联接结构的类型中的"列"数取决于此N,但除非您使用其他类型的数据结构(即List)来表示,否则您将无法动态地执行此操作,即对于任何N,因为C#中的数据是静态声明的。

为了进行说明,请检查下面的查询,其中我假设数组的维度为4:

var query = from d0 in _data[0]
            join d1 in _data[1] on d0.Key equals d1.Key into d1joined
            from d1 in d1joined.DefaultIfEmpty()
            join d2 in _data[2] on d1.Key equals d2.Key into d2joined
            from d2 in d2joined.DefaultIfEmpty()
            join d3 in _data[3] on d2.Key equals d3.Key into d3joined
            from d3 in d3joined.DefaultIfEmpty()
            select new
                     {
                         d0.Key,
                         D0 = d0.Value,
                         D1 = d1.Value,
                         D2 = d2.Value,
                         D3 = d3.Value,
                      };

不要把重点放在联接上,我稍后会解释,但要检查select new运算符。请注意,当Linq组装这个匿名类型时,它必须知道属性(我们的列)的确切数量,因为这是语法的一部分。

因此,如果你愿意,你可以写一个查询来执行你的要求,但它只适用于已知的N值。如果这恰好是一个足够的解决方案,它实际上很简单,尽管我写的例子可能有点过于复杂。回到上面的查询,您将看到从/join/from DefaultIfEmpty的重复模式。这个模式是从这里复制的,实际上它的工作原理很简单:它通过某个键将两个结构连接到另一个结果结构中(上面的into dnjoined)。Linq将处理左边列表中的所有记录,对于其中的每一个,它将处理右边列表中的每一条记录(N1 x N2的笛卡尔平面),如下所示:

foreach (var d0 in _data[0])
{
    foreach (var d1 in _data[1])
    {
        if (d0.Key == d1.Key) 
        {
            // Produces an anonymous structure of { d0.Key, d0.Value, d1.Value }
            // and returns it.
        }
    }
}

因此,内部联接操作与组合每一行,然后选择键匹配的行相同。外部联接的不同之处在于,即使密钥不匹配,也会产生一行,因此在我们的伪代码中,它将类似于:

foreach (var d0 in _data[0])
{
    foreach (var d1 in _data[1])
    {
        if (d0.Key == d1.Key) 
        {
            // Produces an anonymous structure of { d0.Key, d0.Value, d1.Value }
            // and returns it.
        }
        else
        {
            // Produce a anonymous structure of {d0.Key, d0.Value, null}
        }     
    }
}

之前在LINQ代码中通过添加第二个where子句实现了else块,该子句即使在没有匹配的情况下也会请求行,这是一个空列表,当调用DefaultIfEmpty时可以返回数据。(再次,请参阅上面的链接以获取更多信息)

我将在下面复制一个完整的示例,该示例使用我上面提到的数据结构和linq查询。希望它是不言自明的:

using System;
using System.Collections.Generic;
using System.Linq;
namespace TestZone
{
    class Example
    {
        #region Types
        class MyValue
        {
            public int Value { get; set; }
            public override string ToString()
            {
                return string.Format("MyValue(Value = {0})", Value);
            }
        }
        #endregion // Types
        #region Constants
        /// <summary>
        /// Our N
        /// </summary>
        private const int NumberOfArrays = 4;
        /// <summary>
        /// How many rows per dictionary
        /// </summary>
        private const int NumberOfRows = 10; 
        #endregion // Constants
        #region Fields
        private Dictionary<string, MyValue>[] _data = new Dictionary<string, MyValue>[NumberOfArrays]; 
        #endregion // Fields
        #region Constructor
        public Example()
        {
            for (var index = 0; index < _data.Length; index++)
            {
                _data[index] = new Dictionary<string, MyValue>(NumberOfRows);
            }
        } 
        #endregion // Constructor
        public void GenerateRandomData()
        {
            var rand = new Random(DateTime.Now.Millisecond);
            foreach (var dict in _data)
            {
                // Add a number of rows
                for (var i = 0; i < NumberOfRows; i++)
                {
                    var integer = rand.Next(10);    // We use a value of 10 so we have many collisions.
                    dict["ValueOf" + integer] = new MyValue { Value = integer };
                }
            }
        }
        public void OuterJoin()
        {
            // To get the outer join, we have to know the expected N before hand, as this example will show.
            // Do multiple joins
            var query = from d0 in _data[0]
                        join d1 in _data[1] on d0.Key equals d1.Key into d1joined
                        from d1 in d1joined.DefaultIfEmpty()
                        join d2 in _data[2] on d1.Key equals d2.Key into d2joined
                        from d2 in d2joined.DefaultIfEmpty()
                        join d3 in _data[3] on d2.Key equals d3.Key into d3joined
                        from d3 in d3joined.DefaultIfEmpty()
                        select new
                                   {
                                       d0.Key,
                                       D0 = d0.Value,
                                       D1 = d1.Value,
                                       D2 = d2.Value,
                                       D3 = d3.Value,
                                   };
            foreach (var q in query)
            {
                Console.WriteLine(q);
            }
        }
    }
    class Program
    {
        public static void Main()
        {
            var m = new Example();
            m.GenerateRandomData();
            m.OuterJoin();
        }
    }
}

多维数组不实现IEnumerable<T>,因此您将无法使用LINQ。另一方面,锯齿阵列可以由LINQ操作。