views:

594

answers:

3

I've got a class with many string arrays. I'd like to have one generic function which can get me a unique List<string> for a given property. Example:

public class Zoo 
{    
  string Name { get; set;}
  string[] Animals { get; set;}
  string[] Zookeepers { get; set;}
  string[] Vendors { get; set;}
}

I'd like to have a generic function that will get me a distinct List<string> of Animals in List? I want this to be generic, so I can also get a distinct list of Zookeepers and Vendors.

I've been trying this, but it doesn't compile:

public static List<string> GetExtendedList(Func<Zoo, string[]> filter)
{
        var Zoos = QueryZoos(HttpContext.Current);
        return Zoos.Where(z => z.Type == "Active")
            .SelectMany(filter)
            .Distinct()
            .OrderBy(s => s);
    }

Note: this is related to two questions I've asked before, but I'm having trouble merging the information. I previously asked how to query using SelectMany (SO 1229897) and separately asked how to write a generic function which gets a list using Select rather than SelectMany (SO 1278989).

+1  A: 

The best way would be to create a HashSet<String> for each String[] - this would filter out all duplicates.

Since HashSet<T> has a constructor that accepts an IEnumerable<T> you could simply instantiate a HashSet<T> by passing each of your arrays into the constructor. The resulting HashSet<T> would be the distinct list of Strings. While this is not a List<String> like you requested, HashSet<T> does implement ICollection<T> so many of the methods you would need may be available.

static ICollection<String> GetDistinct(IEnumerable<String> sequence)
{
 return new HashSet<String>(sequence);
}
Andrew Hare
I don't need to filter duplicates in each list, I need a de-duped list of child items across all items in the collection. To keep with my analogy, it's fine if each Zoo has a Lion and a Tiger, but I need a function which will get me a list of all Animal types in all my Zoos.
Jon Galloway
Hmm.. I see what you mean. Is refactoring the `Zoo` type an option?
Andrew Hare
It could be, but I'd really like to learn the LINQ solution rather than just restating the problem every time I hit this. Appreciate the input, though!
Jon Galloway
+1  A: 

Maybe I am missing what you mean, but simply...

List<String> distinctAnimals = zoo.Animals.Distinct().ToList();

will do what you are asking, I assume you mean something else?

Edit: If you have a list of Zoos but want the distinct animals then select many is the correct thing to use, IMO its easier using the linq declarative syntax...

List<String> animals = (from z in zoos
                       from s in z.Animals
                       select s).Distinct().ToList();
Tim Jarvis
He's asking for a generic solution, not that Distinct().ToList() aren't generic, take a look at his other posts.
TreeUK
Sorry, my question wasn't clearly stated. I'm looking for a generic solution. I've updated the question statement.
Jon Galloway
Ah I see what you mean now after the Edit. I think that Andrew has the correct answer.
Tim Jarvis
Oops, I Meant David B has the correct answer.
Tim Jarvis
+7  A: 

"Each Zoo"

click

Suppose you had a list of zoo's:

List<Zoo> zooList = GetZooList();

Then, if you wanted distinct animals from all the zoos, you would apply SelectMany in this way:

List<string> animalList = zooList
  .SelectMany(zoo => zoo.animals)
  .Distinct()
  .ToList();

And If you commonly did this task and wanted one function to wrap these three calls, you could write such a function this way:

public static List<string> GetDistinctStringList<T>(
  this IEnumerable<T> source,
  Func<T, IEnumerable<string>> childCollectionFunc
)
{
  return source.SelectMany(childCollectionFunc).Distinct().ToList();
}

Which would then be called:

List<string> animals = ZooList.GetDistinctStringList(zoo => zoo.animals);

And for the code sample that doesn't compile (for which you've given no error message), I deduce you need to add a ToList():

.OrderBy(s => s).ToList();

The other problem (why the type argument can't be inferred) is that string[] doesn't implement IEnumerable<string>. Change that type parameter to IEnumerable<string> instead of string[]

David B
My error message: Error 15 The type arguments for method 'System.Linq.Enumerable.SelectMany<TSource,TResult>(System.Collections.Generic.IEnumerable<TSource>, System.Func<TSource,System.Collections.Generic.IEnumerable<TResult>>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
Jon Galloway
Thanks, this worked great. I'm going to accept and will look at converting it to a non-extension method just to make sure I fully understand it. The main problem I had seems to be in declaring the Func param correctly.
Jon Galloway
Actually, `string[]` does implement `IEnumerable<string>`. The real problem is that `Func<Zoo, string[]>` is a delegate type that is different from `Func<Zoo, IEnumerable<string>>`; and different delegate types are not directly compatible.
Pavel Minaev