简体   繁体   中英

How can I extend Collection to provide a filtered object collection?

I'm trying to create a collection that acts just like all other collections, except when you read out of it, it can be filtered by setting a flag.

Consider this (admittedly contrived) example:

var list = new FilteredStringList() {
    "Annie",
    "Amy",
    "Angela",
    "Mary"
};
list.Filter = true;

I added four items, but then I set the "Filter" flag, so that whenever I read anything out of it (or do anything that involves reading out of it), I want to filter the list to items that begin with "A".

Here's my class:

public class FilteredStringList : Collection<string>
{
    public bool Filter { get; set; }

    protected new IList<string> Items
    {
        get
        {
            if(Filter)
            {
                return base.Items.Where(i => i.StartsWith("A")).ToList();
            }
            return base.Items;
        }
    }

    public IEnumerator GetEnumerator()
    {
        foreach(var item in Items)
        {
            yield return item;
        }
    }
}

My theory is that I'm overriding and filtering the Items property from the base Collection object. I assumed that all other methods would read from this collection.

If I iterate via ForEach, I get:

Annie
Amy
Angela

Yay! But, if I do this:

list.Count()

I get "4". So, Count() is not respecting my filter.

And if I do this:

list.Where(i => i[1] == 'a')

I still get "Mary" back, even though she shouldn't be there.

I know I can override all sorts of the Collection methods, but I don't really want to override them all. Additionally, I want all the LINQ methods to respect my filter.

It sounds like your concept is to have one list that can be represented in two different ways. That's really two lists, and I would suggest running it as two lists. Furthering your contrived example:

// the base list containing all items
var list = new List<string>
{
    "Annie", "Amy", "Angela", "Mary"
};

Then, create an expression of that list that only is your filtered items:

 // do NOT .ToList() this list:
 var filteredList = list.Where (x => x.StartsWith("A"));

You can continue to add/remove items from list and filteredList will always contain the updated values when you access it.

And if you really must access only a single field/property then you could put list and filteredList into a class that also includes an IsFiltered property that is used to determine which of those two lists to return.

What you would be doing is creating a class that returns to you the proper list. Something like:

public class Filtered<T>
{
    private IEnumerable<T> _list;
    private IEnumerable<T> _filteredList;

    public bool IsFiltered { get; set; }

    public IEnumerable<T> MyCollection { get { return IsFiltered ? _filteredList 
                                                                 : _list; }}

    public Filtered(IEnumerable<T> list, IEnumerable<T> filteredList)
    {
        _list = list;
        _filteredList = filteredList;
    }
}

Instead of binding to list like your original example, you're binding to the above class's MyCollection property ( filtered.MyCollection );

Don't inherit from Collection . Instead just inherit directly from Object , have the method implement whatever interfaces are needed ( IList , probably), create an internal List instance to be the backing collection, and then expose each operation as you would like it to be seen.

public class FilteredCollection : IList<string>, IEnumerable<string>
{
    private List<string> list = new List<string>();
    public bool Filter { get; set; }

    protected IList<string> Items
    {
        get
        {
            if (Filter)
            {
                return list.Where(i => i.StartsWith("A")).ToList();
            }
            return list;
        }
    }

    public IEnumerator<string> GetEnumerator()
    {
        return Items.GetEnumerator();
    }

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

    public int IndexOf(string item)
    {
        throw new NotImplementedException();
    }

    public void Insert(int index, string item)
    {
        throw new NotImplementedException();
    }

    public void RemoveAt(int index)
    {
        throw new NotImplementedException();
    }

    public string this[int index]
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    //continue with the rest
}

There are several advantages here.

  1. It's clear to you what each method is doing. You don't need to worry about having methods left with defaults that aren't sensible. The fact that only a handful of operations are likely to literally just calling the same method of the List is a motivating factor here. To truly represent a filtered list just about every single operation likely should change, so it's not like you're adding a bunch of work for yourself.

  2. The methods of Collection aren't virtual, they're sealed, this means that having the class typed as the base class will result in the base class methods being used. This means a caller can bypass the filters you're adding when you think that they're enabled. That sounds like a really bad idea.

My theory is that I'm overriding and filtering the Items property from the base Collection object. I assumed that all other methods would read from this collection.

But you're not overriding Items , you're hiding it. You even needed to call out the fact that you're not overriding it by using the new keyword instead of the override keyword. You couldn't use the override keyword even if you wanted to because all of the methods/properties in Collection are sealed , not virtual . It was designed specifically to prevent you from doing what you're trying to do . (Which is why I generally see little use for it as a class in general.)

I know I can override all sorts of the Collection methods, but I don't really want to override them all.

Well, technically, the only virtual methods that you have to override are ClearItems , InsertItem , RemoveItem , and SetItem (along with Equals , GetHashCode , and ToString from Object ). The rest of the methods/properties aren't virtual, and so cannot be overridden. The only real effective solution is to write them all out.

Why not just use the plain old List and apply Where clause when needed?

var list = new List<string>() {
    "Annie",
    "Amy",
    "Angela",
    "Mary"
};

list.Where(o=>o.StartsWith("A")).Count();

Maybe override the Add method instead? That way you'll cover all bases.

public class FilteredStringList : Collection<string>
{
    public new void Add(string item)
    {
        if (item.StartsWith("A", StringComparison.InvariantCultureIgnoreCase))
            base.Add(item);
    }
}

My theory is that I'm overriding and filtering the Items property from the base Collection object. I assumed that all other methods would read from this collection.

Note that this is a misconception of how hiding with the new modifier works. When you hide a property using protected new IList<string> Items , it doesn't mean the base Items property is now virtual. References to FilteredStringList will use the new method, but references to Collection<string> (including all methods in the base class) will continue to use the "hidden" base method.

Edit per comment

To have a collection that can switch the filter on/off, consider implementing one of the interfaces like ICollection<T> . That way you can apply whatever filtering logic you like (ie, filter on add, filter on retrieve, etc). For example, if the Filter should apply on write, then you'd implement a custom Add , while the GetEnumerator methods would just be a pass-through. If the Filter should apply on read, then vice versa: Add would be a pass-through, and the enumerator methods would implement the filter:

public class FilteredStringList<T> : ICollection<T>
{
    private Collection<T> _internalCollection = new Collection<T>();
    public bool IsFilterActive { get; set; }
    public Predicate<T> Filter { get; set; }

    public void Add(T item)
    {
        _internalCollection.Add(item);
    }

    public void Clear()
    {
        _internalCollection.Clear();
    }

    public int Count
    {
        get { return _internalCollection.Where(item => !IsFilterActive || Filter(item)).Count(); }
    }

    public IEnumerator<T> GetEnumerator()
    {
        foreach (var item in _internalCollection)
        {
            if (!IsFilterActive || Filter(item))
                yield return item;
        }
    }

    // TODO implement other pass-through ICollection<T> methods
}

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