【问题标题】: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# 中包含另一个列表中的元素从通用列表中删除元素?
【发布时间】:2015-04-05 17:37:05
【问题描述】:

我正在尝试通过制作一个简单的程序来学习 C#,该程序会根据用户想要的成分显示寿司卷。即,用户想要一个有螃蟹的卷,程序将输出一个包含螃蟹的寿司卷列表。

我创建了一个 Roll 类

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

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

在 GUI 中,我有一些复选框,每个复选框都从 Control 类调用 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 上有一个帖子 about removing elements from a generic list while iterating over it。 这很好,并为我指明了正确的方向,但不幸的是,我的谓词条件与最佳答案不匹配。 它必须遍历成分列表,我什至不确定这是否可能......

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

颤抖

我已经尝试了 for 循环,但我似乎也无法让枚举工作,我想知道是否有必要仅为这个方法枚举类。

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

【问题讨论】:

    标签: c# list loops predicate


    【解决方案1】:

    要使用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 创建时的所有元素。


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

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

    RemoveAll 方法,你可以看到documentation for here 接受一个谓词,一个Predicate&lt;T&gt;,它基本上是一个委托,一个方法的引用。

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

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

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

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

    All 方法是一个扩展方法,可在 Enumerable 类中找到:Enumerable.All

    它基本上会遍历它所扩展的集合的所有元素,在这种情况下是单个卷的成分集合,并调用谓词函数。如果对于任何元素,谓词返回false,则调用All 的结果也将是false。如果所有调用都返回true,则结果也将是true。请注意,如果集合(成分)为空,则结果也将为 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 方法,它需要一个返回是否删除项目的谓词。因为如果allfalse,我们需要移除roll,我们使用not 运算符! 来反转该值。

    【讨论】:

    • 既然这不是一个作业,我想做一个最合乎逻辑、最易读的解决方案,那么你能详细说明list.RemoveAll(roll =&gt; !ingredients.All(roll.checkForIngredient));是如何工作的吗?
    • 完成了,我已经解释了更多,希望对您有所帮助,如果没有留下更多的 cmets :)
    • 太棒了!感谢您编写一整段代码来解释参数是如何编译的:)
    【解决方案2】:

    由于您既是 C# 新手,又要求提供优雅的解决方案,我将举一个示例,说明如何使用更面向对象的方法来解决此问题。

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

    public class Ingredient
    {
        private string _name;
    
        public string Name
        {
            get { return _name; }
        }
    
        public Ingredient(string name)
        {
            _name = name;
        }
    }
    

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

    接下来,我们将根据本指南修改您的 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)) 之类的操作,这将使您的代码更具表现力和可读性。您将在我要添加的下一堂课中看到这一点,RollCollection

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

    同样,这类事情往往需要一些样板代码,一开始可能看起来过于复杂,但它会使您的类更易于使用。所以让我们添加一个 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 类中。

    编辑:一个更完整(但仍然是简化)的示例,说明您的 Controller 类最终可能是什么样的:

    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);
        }
    }
    

    【讨论】:

    • 好东西!我一直想通过并清理代码,这真的很有帮助。这些是 C# 普遍接受的命名约定吗?另外,这段代码是否遵循 MVC 格式?我也在尽量保持这种风格。
    • 什么是可接受的命名约定实际上取决于您尝试遵循的风格,更重要的是(如果您在团队中工作),您的团队已经达成一致。我正在应用我在学习领域驱动设计时养成的一些习惯,我个人认为这是我作为软件开发人员在职业生涯中学到的最有用的东西。鉴于此,您的命名约定是编写代码,就好像非技术人员必须阅读和理解它一样。您可能会认为名称很长 - 如果这有助于代码不言自明,那完全可以。
    • 至于您对 MVC 的评论,这又是一个偏好问题,取决于您尝试遵循的其他设计模式或风格。就成分和卷类而言,它们完全可以在 MVC 中使用。 RollCollection 本身并不是一个真正的模型,更像是一个 ViewModel。 ViewModel 并不是真正的 MVC 规范的一部分,但它们当然可以(并且可能应该)与它结合使用。
    • 这并不是因为它们太长,我只是不确定 C# 开发人员喜欢如何命名他们的东西。然后我会去阅读领域驱动设计,这是一个很好的卖点。无论如何,这真的是超越,这已被证明是我在网站上发布的最有效的问题^.^
    • 哈,C# 开发人员喜欢如何命名他们的东西和他们应该如何命名他们的东西之间可能存在很大差异。您应该以一种意图揭示的方式命名您的东西,以便其他人只需浏览您的变量和方法名称就可以理解您正在尝试做什么(而不必阅读每一行代码只是为了能够推理它)。我很高兴它有帮助!不要忘记投票给对您有帮助的答案/cmets ;)
    【解决方案3】:

    我正在尝试通过制作一个向用户展示的简单程序来学习 C# 寿司卷给了他们想要的配料。即用户想要一卷 用螃蟹,程序会吐出一份寿司卷的清单 包含螃蟹。

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

    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 扩展方法 .Where过滤您的 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(使用.Where),如果你需要操纵整个,你可以使用.ToList()直接列出而不是遍历集合(一次获取一项)。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-02-19
      • 2015-09-20
      • 2021-06-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多