简体   繁体   中英

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?

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

I've created a Roll class

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

With some getters and setters and other various methods.

In the GUI, I have some checkboxes which each call an update() method from the Control class, which will then need to check a list of rolls against a list of ingredients given by the GUI checkboxes. What I have is this

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

But a System.InvalidOperationException is thrown saying that because the collection was modified, the operation can't execute. OK, that's fair, but then what's the best way to do this? Here on Stack Overflow there's a post about removing elements from a generic list while iterating over it . This was good and pointed me in the right direction, but unfortunately, my predicate condition simply doesn't match the top answer's. It would have to iterate over the ingredients list, and I'm not even sure that's possible...

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

shudder

I've tried the for loop, but I can't seem to get the enumeration to work either, and I wonder if it's even necessary to enumerate the class for just this method.

So I come here to try and find an elegant, professional solution to my problem. Keep in mind that I'm new to C# and I'm not all too familiar with predicate logic or enumeration on classes.

To use RemoveAll you can rewrite your condition to this:

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

This exploits the fact that when the compiler sees this, it will effectively rewrite it to this:

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

Which is what you want. If not all the ingredients are present, remove the roll.

Now, having said that, since you say you're a beginner, perhaps you feel more comfortable keeping your loop, if you could just make it work (ie. stop crashing due to modifying the loop). To do that, just make a copy of the collection and then loop through the copy, you can do this by just modifying the foreach statement to this:

foreach(Roll roll in Rolls.ToList())

This will create a list based copy of the Rolls collection, and then loop on that. The list will not be modified, even if Rolls is, it is a separate copy containing all the elements of Rolls when it was created.


As requested in the comments, I'll try to explain how this line of code works:

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

The RemoveAll method, which you can see the documentation for here takes a predicate, a Predicate<T> , which is basically a delegate, a reference to a method.

This can be a lambda, syntax that creates an anonymous method, using the => operator. An anonymous method is basically a method declared where you want to use it, without a name, hence the anonymous part. Let's rewrite the code to use an anonymous method instead of a lambda:

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

This is the exact same compiled code as for the lambda version above, just using the bit more verbose syntax of an anonymous method.

So, how does the code inside the method work.

The All method is an extension method, found on the Enumerable class: Enumerable.All .

It will basically loop through all the elements of the collection it is extending, in this case the ingredients collection of a single roll, and call the predicate function. If for any of the elements the predicate returns false , the result of calling All will also be false . If all the calls return true , the result will also be true . Note that if the collection (ingredients) is empty, the result will also be true .

So let's try to rewrite our lambda code, which again looked like this:

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

Into a more verbose method, not using the All extension method:

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

    return !all;
});

This now starts to look like your original piece of code, except that we're using the RemoveAll method, which needs a predicate that returns whether to remove the item or not. Since if all is false , we need to remove the roll, we use the not operator ! to reverse that value.

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

Here's my solution to the given problem:

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

You can use the LINQ extension method .Where to filter your collection of 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);
        }
    }
}

From what I see you're trying to remove all rolls that don't contain crab from your list of rolls . A better approach is to filter out those rolls that don't contain crab (using .Where ), you can then use .ToList() if you need to manipulate the whole list directly rather than iterating through the collection (fetching one item at a time).

You should read up on Delegates , Iterators , Extension Methods and LINQ to better understand what's going on under the covers.

Since you are both new to C# but also asked for an elegant solution, I will give you an example of how to solve this using a more object-oriented approach.

First of all, any "thing" of significance should be modeled as a class, even if it has just one property. This makes it easier to extend the behavior later on. You already defined a class for Roll. I would also add a class for Ingredient:

public class Ingredient
{
    private string _name;

    public string Name
    {
        get { return _name; }
    }

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

Note the Name property which only has a getter, and the constructor which accepts a string name . This might look like unnecessary complexity at first but will make your code more straightforward to consume further down the road.

Next, we'll modify your Roll class according to this guideline and give it some helper methods that make it easier for us to check if a roll contains a certain (list of) ingredients:

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

Pay attention to the ContainsIngredient and ContainsIngredients methods here. Now you can do stuff like if (roll.ContainsIngredient(ingredient)) , which will make your code more expressive and more readable. You'll see this in action in the next class that I'm going to add, RollCollection .

You are modeling collections of food to pick from, presumably in the context of a restaurant menu or some similar domain. You might as well go ahead and model just that: a RollCollection. This will allow you to encapsulate some meaningful logic inside of the collection.

Again, this sort of thing tends to require some boilerplate code and may look overly complex at first, but it will make your classes easier to consume. So let's add a 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 is the thing we were really looking for, as it allows you to do something like this:

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

This is simple and clean, just the sort of thing you want to be doing in your presentation layer. The logic to accomplish your requirement is now nicely encapsulated in the RollCollection class.

EDIT : a more complete (but still simplified) example of how your Controller class might end up looking like:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM