使用LINQ在C#中基于一个属性过滤两个列表

7

我有两个对象,分别是卡片交易

Card:
public string CardID {get; set;}
public string TransactionRef {get; set;}

Transaction:
public string TxnID {get; set;}
public string TxnDetails {get; set;}

注意:TransactionRef的格式为Date|TxnID

我还有两个对象的列表:List<Card> cardDetailsList<Transaction> transDetails

cardDetails:
{CardID = '1', TransactionRef = '20150824|Guid1'}
{CardID = '2', TransactionRef = '20150824|Guid2'}
{CardID = '3', TransactionRef = '20150824|Guid3'}

transDetails:
{TxnID = '23', TxnDetails = 'Guid1'}
{TxnID = '24', TxnDetails = 'Guid2'}

我希望能够使用TxnDetails筛选transDetails中的cardDetails,以过滤掉第二个列表中不包含TxnDetails的项目。
这应该是输出:
cardDetails:
 {CardID = '3', TransactionRef = '20150824|Guid3'}

我尝试使用linq这样做:
  cardDetails = cardDetails.Where(x => transDetails.Any(y => x.TransactionRef.Contains(y.TxnDetails) == false)).ToList();

但它总是返回一个空列表。我已经尝试过许多变体的查询,但都没有成功。我知道这个问题以前已经被问过了,在搜索了它们并尝试了它们的解决方案之后,我仍然无法得到正确的结果。

有人能建议我的查询哪里出了问题吗?

注意:我忘记提到的一件事是,这些列表可能包含数千条记录。因此性能也很重要。


是的,它没有返回任何内容。我想仅返回cardDetails中的第三个条目,即不包含第二个列表中任何TxnDetails的条目。 - nitinvertigo
这个查询的性能指数是n的平方。你应该避免在循环内部使用这种循环。尝试连接两个列表,以便使用哈希表。 - Abdullah Nehir
@MatthewWatson实际上在我的代码中,TxnDetails是一个Guid类型,我将其转换为字符串。在这里,我只是将TxnDetails更改为字符串,但没有更新linq查询。 - nitinvertigo
哇!这么多答案,每一个都有效!! :) 但是我不确定对于大量数据,比如列表中的10万个条目,哪一个会更快? - nitinvertigo
会有具有相同交易细节的卡片吗(例如,两张带有“Guid1”的卡片)? - dcastro
显示剩余6条评论
6个回答

6

这应该可以解决问题。

var cards = 
    from card in cardDetails
    let txnDetails = GetTxnDetails(card)
    where ! transDetails.Any(t => t.TxnDetails == txnDetails)
    select card;


static string GetTxnDetails(Card card)
{
    return card.TransactionRef.Split('|')[1];
}

Fiddle: https://dotnetfiddle.net/b9ylFe


优化这个算法的一种方法是事先将所有可能的交易细节存储在哈希集合中。查找应该接近 O(1)(假设哈希码分布公平)而不是 O(n),从而将算法的总复杂度从 O(n * k) 降至 O(n + k)。

var allTxnDetails = new HashSet<string>(transDetails.Select(t => t.TxnDetails));

var cards = 
    from card in cardDetails
    let txnDetails = GetTxnDetails(card)
    where ! allTxnDetails.Contains(txnDetails)
    select card;

Fiddle: https://dotnetfiddle.net/hTYCbj


1
在这种特定情况下,您也可以使用 EndsWith() - Callum Linington

3
这个查询语句可以解决问题:
// Get all card details whose transactionrefs don't contain txndetails from the second list
cardDetails.Where(cd => transDetails.All(ts => !cd.TransactionRef.EndsWith(ts.TxnDetails)))
    .ToList();

但是,您将两个数据组合在一个字段中的具体原因是什么呢?我建议将您Card类中的TransactionRef字段分成两个字段:TransactionDateTransactionID,以避免在查询中进行字符串操作。


1
是的,这是最简单的解决方案。实际上,问题在于“任何”应该改为“所有”。 - daniel
@SaedAmini 实际上,我正在从 Azure 数据库中提取所有数据,它只能以这种格式出现。我的主要关注点是对于列表中的 10 万个条目,性能如何。 - nitinvertigo

2
这个怎么样?
var results = cardDetails.Where(
    card => !transDetails.Any(
        trans => card.TransactionRef.EndsWith("|" + trans.TxnDetails)));

完整演示:

using System;
using System.Linq;

namespace Demo
{
    class Card
    {
        public string CardID;
        public string TransactionRef;
    }

    class Transaction
    {
        public string TxnID;
        public string TxnDetails;
    }

    internal class Program
    {
        private static void Main()
        {
            var cardDetails = new[]
            {
                new Card {CardID = "1", TransactionRef = "20150824|Guid1"},
                new Card {CardID = "2", TransactionRef = "20150824|Guid2"},
                new Card {CardID = "3", TransactionRef = "20150824|Guid3"}
            };

            var transDetails = new[]
            {
                new Transaction {TxnID = "23", TxnDetails = "Guid1"},
                new Transaction {TxnID = "24", TxnDetails = "Guid2"}
            };

            var results = cardDetails.Where(card => !transDetails.Any(trans => card.TransactionRef.EndsWith("|" + trans.TxnDetails)));

            foreach (var item in results)
                Console.WriteLine(item.CardID + ": " + item.TransactionRef);    
        }
    }
}

2

使用方法链语法进行LINQ:

List<Card> result = cardDetails.Where(
    card => !transDetails.Exists(
         tran => tran.TxnDetails == card.TransactionRef.Split('|')[1]
)).ToList();

你的查询有什么问题?
 cardDetails = cardDetails.Where(x => transDetails.Any(y => x.TransactionRef.Contains(y.TxnDetails) == false)).ToList();

以下是您所写的内容:

找到所有满足此条件的卡片: 我的交易列表中是否有任何交易,其 TxnDetails 不能在该特定卡片的 TxnDetails 中找到?

我看到问题在这里:

如果任何一笔交易的 TxnId 不是卡片 (可能性很高),则返回此卡片。

因此,基本上,如果您的交易列表中至少有两个不同的交易 ID,则应从查询中获取所有卡片。


1
这只是一个括号问题,== false 应该放在 )) 后面而不是第一个闭合括号之后。
cardDetails = cardDetails.Where(x => transDetails.Any(y => x.TransactionRef.Contains(y.TxnDetails)) == false).ToList();

因为你的实际代码与你想要的相反!你也可以这样做。
cardDetails = cardDetails.Where(x => !transDetails.Any(y => x.TransactionRef.Contains(y.TxnDetails))).ToList();

或者提出任何改进建议,但你的代码基本上非常接近正确;)


是的,你说得对! :) 我进行了更改,现在它可以工作了。谢谢。 - nitinvertigo

0
如果性能很重要,我建议您首先给类Card一个属性,该属性返回“|”字符后面的部分。根据您想要执行此查询的频率与构造Card的频率相比如何,甚至让构造函数将transactionRef分成“|”之前的部分和“|”之后的部分可能是更明智的选择。
无论您选择哪种方法对于查询都不重要。假设类Card具有一个属性:
string Guid {get {return ...;}

我理解您想要从cardDetails序列中获取所有卡片的序列,这些卡片没有与transDetails序列中的任何TxnDetails相等的Guid。

换句话说:如果您将所有使用过的TxnDetails的guid序列化,您希望在CardDetails中找到所有具有不在所有使用过的guid序列中的guid的卡片。

您可以使用Any()来实现此目的,但这意味着您必须为每个要检查的卡片搜索transDetails序列。

每当您需要检查特定项是否在序列中时,最好将序列转换为Dictionary或HashSet。您创建哪个取决于您是否仅需要键或具有键的元素。只需创建一次字典/哈希集,并快速搜索具有键的项目。

在我们的情况下,我们只需要一个包含使用过的guid的序列,无论它在哪个交易中使用。

var usedGuids = transDetails.Select(transDetail => transDetail.TxnDetails).Distinct();
var hashedGuids = new HashSet(usedGuids);

我写了两个语句,以便更容易理解所做的事情。

现在,每当我有一个GUID时,我可以非常快速地检查它是否被使用:

bool guidIsUsed = usedGuids.Contains(myGuid);

因此,您在cardDetails中具有GUID的卡片序列不在transDetails中:

var hashedGuids = new HashSet(transDetails.Select(transDetail => transDetail.TxnDetails).Distinct());
var requestedCards = cardDetails.Where(card => !hashedGuids.Contains(card.Guid));

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