在 C# 列表中识别唯一值

3
我创建了一个类来表示复合主键模型,如下所示:
public class PrimaryKeyModel
{
    public string ColumnName { get; set; }
    public string ColumnValue { get; set; }
    public int RowNumber { get; set; } // always unique
}

它基本上代表了一起构成主键的列的名称/值,以及此组合所属的行号;最初在电子表格中。然后我将此模型放入列表中,并使用电子表格中的数据填充它:
List<PrimaryKeyModel> primaryKeysList = new List<PrimaryKeyModel>; 

我想检查primaryKeysList并查看它是否有任何重复的值,如果有,我想知道这些值重复的行号。
我尝试了不同的方法,比如将这个列表加载到HashSet、字典中,并使用this solution here at this link,但都没有成功。有没有什么办法可以解决这个问题。
谢谢。
更新 - 这里是一个样本数据显示。UniqueColumnsModel与PrimaryKeyModel相同;我在这里进行了更改,以使其更清晰。

enter image description here

编辑:澄清问题

我正在尝试从电子表格中导入数据(可以有多种类型:销售、报价等),并将其导入数据库。数据库中的配置表确定了电子表格中哪些列将构成目标表中的主键。我的任务是创建一个例程,在使用我的应用程序上传(导入)电子表格数据之前验证它。我想验证设置为主键组合的列不包含任何重复数据,以便在插入时在目标表中不违反主键约束。

此处提到的列表(PrimaryKeyModel)包含电子表格中列的名称(与其他列一起构成主键)、电子表格中列的值以及该值存在的电子表格行号。该列表通过foreach row / foreach column 循环进行填充。因此,我希望这更好地阐述了问题。


这是一个非常好的机会,在列表上使用BinarySearch,传递一个自定义的PrimaryKeyModel比较器。BinarySearch返回补码结果,指示项目存在的索引。 - Haney
1
@DavidHaney 首先,二分查找是用于查找单个项,而不是查找重复项的。其次,这需要有排序的数据,而这似乎不是本例的情况。 - Servy
1
@DavidHaney 这样做的性能将会非常糟糕。这是O(n^2)的时间复杂度,而实际上可以用O(1)的时间复杂度来解决,而且需要更多的工作量。你基本上只是在描述插入排序算法。 - Servy
1
我假设你所定义的“重复”是指“具有相同的ColumnNameColumnValue的项目”? - D Stanley
2
@t_plusplus:说实话,整个问题非常令人困惑,部分原因是由于您的类名选择不当。强烈建议您提供一些示例输入和预期输出。 - Jon Skeet
显示剩余12条评论
4个回答

4

GroupBy在这方面工作得很好:

primaryKeysList.GroupBy(pk => new {pk.ColumnName, pk.ColumnValue})
               .Where(g => g.Count() > 1)
               .SelectMany(g => g);   // flatten the groups into a single list

如果您查看更新后的问题,这将产生许多错误的结果。 - Bas

2

编辑:我可能误读了问题,并从您的类名PrimaryKeyModel中推断出了太多——我解释为它是主键模型,并且您想要查找重复的主键。如果不是这种情况,请考虑重新命名... 在那时,D Stanley的答案可能是您想要的,但您应该将ColumnName/ColumnValue视为此处的“主键”——行号在逻辑上不是关键部分。


原始答案

您似乎没有重写Equals(object)GetHashCode——这意味着每个对象都被认为与其他对象不同。您可能需要像这样的东西:

public sealed class PrimaryKeyModel : IEquatable<PrimaryKeyModel>
{
    // TODO: Make these read-only (mutable keys are a bad idea...)
    public string ColumnName { get; set; }
    public string ColumnValue { get; set; }
    public int RowNumber { get; set; }

    public override bool Equals(object other)
    {
        return Equals(other as PrimaryKeyModel);
    }

    public bool Equals(PrimaryKeyModel other)
    {
        return other != null &&
               ColumnName == other.ColumnName &&
               ColumnValue == other.ColumnValue &&
               RowNumber == other.RowNumber;
    }

    public override int GetHashCode()
    {
        int hash = 23;
        hash = hash * 31 + ColumnName == null ? 0 : ColumnName.GetHashCode();
        hash = hash * 31 + ColumnValue == null ? 0 : ColumnValue.GetHashCode();
        hash = hash * 31 + RowNumber;
        return hash;
    }
}

假设您确实希望所有三个字段相同——如果您只关心RowNumber,可以简化这些实现(但此时它是一个奇怪的主键)。
之后,您可以使用Distinct(),或HashSet,或Dictionary等。当然,另一种选择是显式地按不同属性分组,但感觉应该明智地实现相等性。正如评论中所指出的那样,我建议将属性设置为只读。

1
Equals/GetHashCode 的实现很棒,但问题要求查找重复的“名称/值”对及其行号。 - D Stanley
@DStanley:嗯,我想我明白了,如果是这样,PrimaryKeyModel是一个非常糟糕的类名。不过还是不太清楚——会进行编辑。 - Jon Skeet

2
如果您的类表示这种结构:
ColumnName    ColumnValue   RowNumber
Id            3             1
Id2           1             1 
Id            1             2 
Id2           2             2
Id            3             3 
Id2           1             3 //duplicate

那么到目前为止,所有其他答案都是不正确的,您需要以行号分组,然后逐个比较每个字段。因为相等性是可交换的,所以我们可以稍微加快循环速度,这样我们就不必将每个项都比较两次。
List<PrimaryKeyModel> keys = new List<PrimaryKeyModel>()
{
        new PrimaryKeyModel("Id", "3", 1),
        new PrimaryKeyModel("Id2", "1", 1),
        new PrimaryKeyModel("Id", "1", 2),
        new PrimaryKeyModel("Id2", "1", 2),
        new PrimaryKeyModel("Id", "3", 3),
        new PrimaryKeyModel("Id2", "1", 3),
};

var groupedKeys = keys.OrderBy(pk => pk.ColumnName).GroupBy(k => k.RowNumber).ToList();
HashSet<int> duplicateRowNumbers = new HashSet<int>();

for (int i = 0; i < groupedKeys.Count - 1; i++)
{
    for (int j = i + 1; j < groupedKeys.Count; j++)
    {
        if (AreTheSame(groupedKeys[i], groupedKeys[j]))
        {
            duplicateRowNumbers.Add(groupedKeys[i].First().RowNumber);
            duplicateRowNumbers.Add(groupedKeys[j].First().RowNumber);
        }
    }
}

private static bool AreTheSame(IEnumerable<PrimaryKeyModel> a, IEnumerable<PrimaryKeyModel> b)
{
    var leftEnumerator = a.GetEnumerator();
    var rightEnumerator = b.GetEnumerator();
    while (leftEnumerator.MoveNext() | rightEnumerator.MoveNext())
    {
        if (leftEnumerator.Current == null) return false;
        if (rightEnumerator.Current == null) return false;
        if (leftEnumerator.Current.ColumnValue != rightEnumerator.Current.ColumnValue) return false;
    }

    return true;
}

经过对不同数据模式的进一步测试,我发现当“组合”列具有重复值时,此解决方案会失败。它可能需要进行一些调整,因为它目前是单独测试每个列,而不是集体测试。对于数据行中的每次迭代,所有列在一起必须给出唯一值。无论如何,谢谢,我已经在下面放置了我的解决方案。@Bas Brekelmans - t_plusplus
我认为你的解释可能存在问题,我的解决方案按名称对列进行排序,并且仅在两个行号中所有列值对都完全相同时返回true。@t_plusplus - Bas
谢谢。我会再次检查并查看为什么在某些数据模式下失败了。@Bas Brekelmans - t_plusplus

0
这是对我有效的最终解决方案。它确保在列表(即列表的列表)的行中不存在重复项。它基本上将列表的内容倾诉到哈希集中,如果新添加的项已经存在于列表中,则返回false:
感谢所有为解决此问题做出贡献的人!
HashSet<string> primaryKeyChecker = new HashSet<string>();

foreach (var row in rows)
{

    StringBuilder primaryKey = new StringBuilder();
    //Get rowCount;

    foreach (var column in columns)
    {
        (if column is a composite of a primaryKey)
        {
            get column value;
            append it to stringBuilder to form the primaryKey
        }   
    }

                            var addOutcome = primaryKeyChecker.Add(primaryKey.ToString());

                            if (!addOutcome)
                            {
                                //Report a duplicate record and give the rowNumber where this occured.
                            }


}

更新

为了解决以下与@Bas所提出的问题,请确保在连接主键时,使用逗号或0将它们分开,以避免发生类似的情况。例如:

  primaryKey.Append(currentValue + ",");

这段代码会因为连接字符串并报告重复而在输入(21,2)和(2,12)时出现错误。 - Bas
感谢您向我指出这个问题,我已经在上面的更新中提供了解决方案 @Bas Brekelmans - t_plusplus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接