在C#中寻找最佳匹配实例的最佳做法

4

对于大多数人来说,这是一个非常简单的问题,但目前我仍在为解决方案而苦苦挣扎。

假设你有一份猫的列表(List),每只猫都有一份小猫的列表(Kitten)。

public class Cat
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Race { get; set; }
        public bool Gender { get; set; }
        public List<Kitten> Babys { get; set; }  
    }

public class Kitten
    {
        public string Name { get; set; }
        public double Age { get; set; }
        public bool Gender { get; set; }
    }

现在我想找到符合给定要求的猫中匹配度最高的一只猫。很可能有只猫只符合3个要求中的2个,我只想找到与我的要求匹配度最高的那只猫。

我的要求可以是:

  • 名字必须是"Micky"
  • 年龄为42岁
  • 拥有一只名叫"Mini"的小猫咪

我的实际解决方案是比较所有属性,并选择具有最高匹配属性计数的属性。但这不是通用的,我相信有更好的方法来做到这一点。

提前感谢


2
你尝试过什么吗?“给定要求”是什么?C#已经在LINQ中拥有了通用的查询机制。你可以在Where()函数中传递任何谓词,使用GroupBy()进行分组等。 - Panagiotis Kanavos
需求可能是猫的姓名和年龄。我会尝试使用LINQ...但我不是很熟悉它。 - Locke
当你说“非泛型”时,为什么它需要是非泛型的?是否有其他类型需要进行类似的匹配比较? - auburg
2
没有“通用”的方法。如果您只有简单的属性比较,那么可以实现一些基于反射的解决方案。它可以工作,但速度会很慢。然而,您有一些“高级规则”(查看集合类型的属性并在其中执行搜索)。无论如何,您都需要向编译器“解释”这些规则,因此必须动态编写代码或构造表达式。 - dymanoid
2
你可以让那只猫本身给你一个类似于“比较分数”的东西。实现一个方法,接受参数(也许是一个Func?),并返回代表它与这些参数匹配程度的分数。 - nilsK
显示剩余5条评论
4个回答

2

好的,我没有机会测试这个解决方案,但你可以尝试一下:

假设你有一组猫的列表:

var cats = new List<Cat>();

现在你已经定义了你的标准:

var desiredName = "Micky";
var desiredAge = 42;
var desiredKitten = "Mini";

接下来你需要获取你想要的猫:

var desiredCat = cats
        .Select(c => new {
            Rating = 
                Convert.ToInt32(c.Age == desiredAge) +       // Here you check first criteria
                Convert.ToInt32(c.Name == desiredName) +     // Check second
                Convert.ToInt32(c.Babys.Count(b => b.Name == desiredKitten) > 0),   // And the third one
            c })
        .OrderByDescending(obj => obj.Rating) // Here you order them by number of matching criteria
        .Select(obj => obj.c) // Then you select only cats from your custom object
        .First(); // And get the first of them

请检查一下这是否适用于您。 如果您需要更具体的答案或者需要我添加一些编辑,请告诉我。

第一次测试看起来很不错!谢谢。我希望有一个可以自动处理新属性的解决方案。如果我找到了这个(下一步)的解决方案,我会发布它的。 - Locke
@Locke 你可以创建一个条件列表,其中一个是返回0或1的通用函数,并以 Cat 作为参数,或者类似的东西,这只是一个想法。 - Markiian Benovskyi

0
如果您要比较2或3个需求,可以使用Linq进行简化:

// try to find with 3 requirements
var foundCats = catList.Where(t => t.Name == desiredName && 
                                   t.Age == desiredAge &&
                                   t.Babys.Any(k => k.Name == desiredKitten)
                             ).ToList();

if (foundCats.Any())
{
    // you found the desired cat (or cats)
    return foundCats;
}

// try to find with 2 requirements
foundCats = catList.Where(t => 
    (t.Name == desiredName && t.Age == desiredAge) ||
    (t.Name == desiredName && t.Babys.Any(k => k.Name == desiredKitten)) ||
    (t.Age == desiredAge && t.Babys.Any(k => k.Name == desiredKitten)
).ToList();

if (foundCats.Any())
{
    // you found the desired cat (or cats)
    return foundCats;
}

// try to find with only 1 requirement
foundCats = catList.Where(t => t.Name == desiredName || 
                               t.Age == desiredAge ||
                               t.Babys.Any(k => k.Name == desiredKitten)
                         ).ToList();
return foundCats;

4
这正是 OP 试图避免的。 - dymanoid
@CharlesCavalcante 我有一个看起来与你的解决方案相似的解决方案,但如果出现新属性...我必须制作第三个谓词,依此类推,想象一下你有100个属性..这将是一个可怕的场景;-) - Locke

0

所以,我看到问题是你不知道在不久的将来是否会有更多的属性,所以我建议采用困难的方法并进行反射,以下内容很丑陋,但你可能(应该)可以使其更好,并希望它作为指南能够为你服务:

public static List<Cat> CheckProperties(List<Cat> inCatList, Cat inQueryCat)
{
    Dictionary<Cat, List<PropertyInfo>> dict = new Dictionary<Cat, List<PropertyInfo>>();

    foreach (PropertyInfo pI in inQueryCat.GetType().GetProperties())
    {
        var value = pI.GetValue(inQueryCat);

        if (value != null)
        {
            var cats = inCatList.Where(cat => cat.GetType().GetProperty(pI.Name).GetValue(cat).Equals(value));

            foreach (Cat cat in cats)
            {
                if (dict.ContainsKey(cat))
                {
                    dict[cat].Add(pI);
                }
                else
                {
                    dict.Add(cat, new List<PropertyInfo>() {pI});
                }
            }
        }
    }

    int max = Int32.MinValue;
    foreach (KeyValuePair<Cat, List<PropertyInfo>> keyValuePair in dict)
    {
        if (keyValuePair.Value.Count > max)
        {
            max = keyValuePair.Value.Count;
        }
    }

    return dict.Where(pair => pair.Value.Count == max).Select(pair => pair.Key).ToList();
}

0

虽然这是最通用的解决方案(需要一些边缘情况的改进):

  public class ReflectCmpare
    {
        public PropertyInfo PropertyInfo { get; set; }
        public dynamic Value { get; set; }
    }


    public Cat GetBestCat(List<Cat> listOfCats, List<ReflectCmpare> catParamsToCompare, List<ReflectCmpare> kittensParamsToCompare)
    {
        var bestScore = 0;
        var ret = listOfCats[0];
        foreach (var cat in listOfCats)
        {
            var score = catParamsToCompare.Sum(param => param.PropertyInfo.GetValue(cat, null) == param.Value ? 1 : 0);
            foreach (var baby in cat.Babys)
            {
                score+= kittensParamsToCompare.Sum(param => param.PropertyInfo.GetValue(baby, null) == param.Value ? 1 : 0);
            }

            if (score <= bestScore) continue;
            bestScore = score;
            ret = cat;
        }
        return ret;
    }

考虑到这些对象不是动态的,你应该真正考虑只做简单的比较函数,这是正确的方法:

public Cat GetBestCat(List<Cat> listOfCats, string name , int? age , bool? gender, string race ,string babyName,int? babyAge,bool? babyGender )
    {
        var ret = listOfCats[0];
        var highestScore = 0;
        foreach (var cat in listOfCats)
        {
            var score = 0;
            score += name != null && cat.Name.Equals(name) ? 1 : 0;
            score += age.HasValue && cat.Age.Equals(age.Value) ? 1 : 0;
            score += gender.HasValue && cat.Gender.Equals(gender.Value) ? 1 : 0;
            score += race != null && cat.Race.Equals(race) ? 1 : 0;
            score += name != null && cat.Name.Equals(name) ? 1 : 0;
            score += cat.Babys
                .Where(k => babyName==null || k.Name.Equals(babyName))
                .Where(k => !babyAge.HasValue || k.Age.Equals(babyAge.Value))
                .Any(k => !babyGender.HasValue || k.Gender.Equals(babyGender.Value))?1:0;
            if (score <= highestScore) continue;
            highestScore = score;
            ret = cat;
        }

        return ret;
    }

Babys 集合中的搜索是硬编码的。如果出现新属性 List<Cat> Parents 和新的条件 "父母的名字是 'Micky' 和 'Felix'",该怎么办?解决方案并不像你所说的那样“最通用”。 - dymanoid
实际上,Nekeniehl的回答是你所寻找的,并且比我的反射建议要简单得多。 只需创建所需的inqueryCat并使用反射进行比较即可。 请注意,inquryCat应该是一个具有可空属性的新类。 - Mbjahnoon

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