我应该如何使用谓词逻辑基于列表对象包含 C# 中另一个列表的元素从通用列表中删除元素?

How should I remove elements from a generic list based on the list s object's inclusion of elementfrom another list in C# using predicate logic?

我正在尝试通过制作一个简单的程序来学习 C#,该程序向用户展示给定所需配料的寿司卷。即用户想要一个带螃蟹的卷,程序将吐出包含螃蟹的寿司卷列表。

我创建了一个 Roll class

public class Roll
{ 
    private string name;
    private List<string> ingredients = new List<string>();
}

使用一些 getter 和 setter 以及其他各种方法。

在 GUI 中,我有一些复选框,每个复选框都从控件 class 调用 update() 方法,然后需要根据 GUI 复选框给出的配料列表检查面包卷列表.我有的是这个

class Controller
{
        static List<Roll> Rolls = new List<Roll>();
        static RollList RL = new RollList();
        static List<String> ingredients = new List<String>();
        static Roll roll = new Roll();
}
public void update
{
    foreach(Roll roll in Rolls)
                {
                    foreach (String ingredient in ingredients)
                        if (!roll.checkForIngredient(ingredient)) 
                            Rolls.Remove(roll);
                }
}

但是抛出一个System.InvalidOperationException说因为集合被修改,操作无法执行。好吧,这很公平,但是最好的方法是什么?在 Stack Overflow 上有一个 post about removing elements from a generic list while iterating over it。 这很好,为我指明了正确的方向,但不幸的是,我的谓词条件与最佳答案不符。 它必须遍历成分列表,我什至不确定这是否可能...

list.RemoveAll(roll => !roll.containsIngredient(each string ingredient in ingredients) );

不寒而栗

我已经尝试了 for 循环,但我似乎也无法使枚举工作,我想知道是否有必要仅针对此方法枚举 class。

所以我来这里是为了尝试找到一个优雅、专业的解决方案来解决我的问题。请记住,我是 C# 的新手,我不太熟悉 classes 上的谓词逻辑或枚举。

要使用 RemoveAll,您可以将条件重写为:

list.RemoveAll(roll => !ingredients.All(roll.checkForIngredient));

这利用了这样一个事实,即当编译器看到它时,它会有效地将其重写为:

list.RemoveAll(roll => !ingredients.All(i => roll.checkForIngredient(i)));

这就是你想要的。如果不是所有成分都存在,请取出卷。

话虽如此,既然你说你是初学者,也许你觉得保持你的循环更舒服,如果你能让它工作(即停止由于修改循环而崩溃)。为此,只需复制该集合,然后循环遍历该副本,只需将 foreach 语句修改为:

foreach(Roll roll in Rolls.ToList())

这将创建一个基于列表的 Rolls 集合副本,然后在其上循环。该列表不会被修改,即使 Rolls 被创建,它也是一个单独的副本,包含 Rolls 创建时的所有元素。


根据评论中的要求,我将尝试解释这行代码的工作原理:

list.RemoveAll(roll => !ingredients.All(roll.checkForIngredient));

RemoveAll 方法,您可以看到 documentation for here 接受一个谓词,一个 Predicate<T>,它基本上是一个委托,对方法的引用。

这可以是使用 => 运算符创建匿名方法的 lambda 语法。匿名方法基本上是在您要使用它的地方声明的方法,没有名称,因此是匿名部分。让我们重写代码以使用匿名方法而不是 lambda:

list.RemoveAll(delegate(Roll roll)
{
    return !ingredients.All(roll.checkForIngredient);
});

这是与上述 lambda 版本完全相同的编译代码,只是使用了匿名方法的更详细的语法。

那么,方法中的代码是如何工作的。

All 方法是一种扩展方法,在 Enumerable class: Enumerable.All 上找到。

它基本上会遍历它正在扩展的集合的所有元素,在本例中是单个卷的成分集合,并调用谓词函数。如果对于任何元素谓词 returns false,调用 All 的结果也将是 false。如果全部调用returntrue,结果也会是true。请注意,如果集合 (ingredients) 为空,则结果也将是 true.

所以让我们尝试重写我们的 lambda 代码,它再次看起来像这样:

list.RemoveAll(roll => !ingredients.All(roll.checkForIngredient));

进入更详细的方法,不使用 All 扩展方法:

list.RemoveAll(delegate(Roll roll)
{
    bool all = true;
    foreach (var ingredient in ingredients)
        if (!roll.checkForIngredient(ingredient))
        {
            all = false;
            break;
        }

    return !all;
});

这现在开始看起来像您的原始代码片段,只是我们使用的是 RemoveAll 方法,该方法需要一个谓词 return 是否删除该项目。因为如果 allfalse,我们需要删除滚动,我们使用非运算符 ! 来反转该值。

I am trying to learn C# by making a simple program that shows the user sushi rolls given their desired ingredients. i.e. a user wants a roll with crab, and the program will spit out a list of sushi rolls that contain crab.

这是我对给定问题的解决方案:

public class Roll
{
    public string Name { get; set; }
    private List<string> ingredients = new List<string>();
    public IList<string> Ingredients { get { return ingredients; } }

    public bool Contains(string ingredient)
    {
        return Ingredients.Any(i => i.Equals(ingredient));
    }
}

您可以使用 LINQ 扩展方法 .Wherefilter 您的 Rolls

public class Program
{
    static void Main()
    {

        var allRolls = new List<Roll>
        {
            new Roll
            {
                Name = "Roll 1",
                Ingredients = { "IngredientA", "Crab", "IngredientC" }
            },
            new Roll
            {
                Name = "Roll 2",
                Ingredients = { "IngredientB", "IngredientC" }
            },
            new Roll
            {
                Name = "Roll 3",
                Ingredients = { "Crab", "IngredientA" }
            }
        };


        var rollsWithCrab = allRolls.Where(roll => roll.Contains("Crab"));
        foreach (Roll roll in rollsWithCrab)
        {
            Console.WriteLine(roll.Name);
        }
    }
}

据我所知,您正试图从 中删除所有不包含 crabrolls卷列表。更好的方法是过滤掉那些 rolls 不包含 crab (使用 .Where),然后你可以使用 .ToList() 如果您需要直接操作整个列表而不是遍历集合(一次获取一项)。

您应该阅读 DelegatesIteratorsExtension Methods 和 LINQ 以便更好了解幕后发生的事情。

由于你们都是 C# 的新手,但又要求一个优雅的解决方案,我将举例说明如何使用更面向对象的方法来解决这个问题。

首先,任何重要的 "thing" 都应该建模为 class,即使它只有一个 属性。这使得以后扩展行为变得更加容易。您已经为 Roll 定义了 class。我还要为成分添加一个 class:

public class Ingredient
{
    private string _name;

    public string Name
    {
        get { return _name; }
    }

    public Ingredient(string name)
    {
        _name = name;
    }
}

注意只有 getter 的名称 属性,以及接受字符串 name 的构造函数。乍一看,这可能看起来像是不必要的复杂性,但会让您的代码更直接,以便在以后使用。

接下来,我们将根据此指南修改您的 Roll class 并为其提供一些辅助方法,以便我们更轻松地检查 roll 是否包含某个(列表)成分:

public class Roll
{
    private string _name;
    private List<Ingredient> _ingredients = new List<Ingredient>();

    public string Name
    {
        // By only exposing the property through a getter, you are preventing the name
        // from being changed after the roll has been created
        get { return _name; }
    }

    public List<Ingredient> Ingredients
    {
        // Similarly here, you are forcing the consumer to use the AddIngredient method
        // where you can do any necessary checks before actually adding the ingredient
        get { return _ingredients; }
    }

    public Roll(string name)
    {
        _name = name;
    }

    public bool AddIngredient(Ingredient ingredient)
    {
        // Returning a boolean value to indicate whether the ingredient was already present,
        // gives the consumer of this class a way to present feedback to the end user
        bool alreadyHasIngredient = _ingredients.Any(i => i.Name == ingredient.Name);
        if (!alreadyHasIngredient)
        {
            _ingredients.Add(ingredient);
            return true;
        }
        return false;
    }

    public bool ContainsIngredients(IEnumerable<Ingredient> ingredients)
    {
        // We use a method group to check for all of the supplied ingredients 
        // whether or not they exist
        return ingredients.All(ContainsIngredient);
        // Could be rewritten as: ingredients.All(i => ContainsIngredient(i));
    }

    public bool ContainsIngredient(Ingredient ingredient)
    {
        // We simply check if an ingredient is present by comparing their names
        return _ingredients.Any(i => i.Name == ingredient.Name);
    }
}

注意这里的ContainsIngredient和ContainsIngredients方法。现在您可以执行 if (roll.ContainsIngredient(ingredient)) 之类的操作,这将使您的代码更具表现力和可读性。在我要添加的 class RollCollection.

中,您将看到它的实际效果

您正在对可供选择的食物集合进行建模,大概是在餐厅菜单或某个类似领域的上下文中。您还不如继续建模:一个 RollCollection。这将允许您在集合中封装一些有意义的逻辑。

同样,这类事情往往需要一些样板代码,一开始可能看起来过于复杂,但它会让您的 classes 更易于使用。所以让我们添加一个 RollCollection:

public class RollCollection : IEnumerable<Roll>
{
    private List<Roll> _rolls = new List<Roll>();

    public RollCollection()
    {
        // We need to provide a default constructor if we want to be able
        // to instantiate an empty RollCollection and then add rolls later on
    }

    public RollCollection(IEnumerable<Roll> rolls)
    {
        // By providing a constructor overload which accepts an IEnumerable<Roll>,
        // we have the opportunity to create a new RollCollection based on a filtered existing collection of rolls
        _rolls = rolls.ToList();
    }

    public RollCollection WhichContainIngredients(IEnumerable<Ingredient> ingredients)
    {
        IEnumerable<Roll> filteredRolls = _rolls
            .Where(r => r.ContainsIngredients(ingredients));

        return new RollCollection(filteredRolls);
    }

    public bool AddRoll(Roll roll)
    {
        // Similar to AddIngredient
        bool alreadyContainsRoll = _rolls.Any(r => r.Name == roll.Name);
        if (!alreadyContainsRoll)
        {
            _rolls.Add(roll);
            return true;
        }
        return false;
    }

    #region IEnumerable implementation

    public IEnumerator<Roll> GetEnumerator()
    {
        foreach (Roll roll in _rolls)
        {
            yield return roll;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion
}

WhichContainIngredients 是我们真正想要的东西,因为它允许你做这样的事情:

    // I have omitted the (proper) instantiation of Rolls and ChosenIngredients for brevity here
    public RollCollection Rolls { get; set; }

    public List<Ingredient> ChosenIngredients { get; set; } 

    public void Update()
    {
        Rolls = Rolls.WhichContainIngredients(ChosenIngredients);
    }

这简单明了,正是您想在表示层中做的事情。完成您的要求的逻辑现在很好地封装在 RollCollection class.

编辑:一个更完整(但仍然简化)的示例,说明您的控制器 class 最终可能看起来像:

public class Controller
{
    private RollCollection _availableRolls = new RollCollection();
    private List<Ingredient> _availableIngredients = new List<Ingredient>();

    public RollCollection AvailableRolls
    {
        get { return _availableRolls; }
    }

    public List<Ingredient> AvailableIngredients
    {
        get { return _availableIngredients; }
    }

    public RollCollection RollsFilteredByIngredients
    {
        get { return AvailableRolls.WhichContainIngredients(ChosenIngredients); }
    }

    public List<Ingredient> ChosenIngredients { get; set; }

    public Controller()
    {
        ChosenIngredients = new List<Ingredient>();
        InitializeTestData();
    }

    private void InitializeTestData()
    {
        Ingredient ingredient1 = new Ingredient("Ingredient1");
        Ingredient ingredient2 = new Ingredient("Ingredient2");
        Ingredient ingredient3 = new Ingredient("Ingredient3");
        _availableIngredients.Add(ingredient1);
        _availableIngredients.Add(ingredient2);
        _availableIngredients.Add(ingredient3);

        Roll roll1 = new Roll("Roll1");
        roll1.AddIngredient(ingredient1);
        roll1.AddIngredient(ingredient2);

        Roll roll2 = new Roll("Roll2");
        roll2.AddIngredient(ingredient3);

        _availableRolls.AddRoll(roll1);
        _availableRolls.AddRoll(roll2);
    }
}