Home > Software engineering >  c# extension methods for generic array
c# extension methods for generic array

Time:06-21

I'm trying to make extension methods for generic array, so I could takeout random set of elements.
I made following extension methods for List<T> type and they work great, but I can't work out how to do exactly the same for generic array:

public static T Random<T>(this List<T> list)
{
    return list[GameManager.instance.functions.RandomInt(list.Count - 1)];
}

public static IEquatable Random<IEquatable>(this List<IEquatable> list, List<IEquatable> hits)
{
    int rand = GameManager.instance.functions.RandomInt(list.Count - 1);
    while (hits.Exists(h => h.Equals(list[rand])))
        rand = GameManager.instance.functions.RandomInt(list.Count - 1);
    return list[rand];
}

public static List<T> Random<T>(this List<T> list, int count)
{
    List<T> result = new List<T>();
    for (int i = 0; i < count; i  )
    {
        result.Add(list.Random());
    }
    return result;
}

public static List<IEquatable> RandomUnique<IEquatable>(this List<IEquatable> list, int count)
{
    List<IEquatable> result = new List<IEquatable>();
    for (int i = 0; i < count; i  )
    {
        result.Add(list.Random(result));
    }
    return result;
}

I tried to rework the first method like this:

public static IEnumerable Random<IEnumerable>(this IEnumerable list)

but it doesn't recognize list as an array so I can't get to it's length value.
I see a workaround, to do a List from Array, then get my random values and make array again, but it's seems like too much action for just taking eg. 2 random from 4 elements array.
Please advise

EDIT:
Thanks to Mathew in comments, I managed to construct the extension method for generic array correctly:

public static T Random<T>(this T[] list)
{
    return list[GameManager.instance.functions.RandomInt(list.Length - 1)];
}

But ultimately I'll play around with the Dmitry's answer and try to make these for IEnumerable. Thank you very much!

EDIT2:
Thanks to Zastai, I changed all methods so they work for both List and generic array:

public static T Random<T>(this IReadOnlyList<T> list)
{
    return list[GameManager.instance.functions.RandomInt(list.Count - 1)];
}

public static IEquatable Random<IEquatable>(this IReadOnlyList<IEquatable> list, List<IEquatable> hits)
{
    int rand = GameManager.instance.functions.RandomInt(list.Count - 1);
    while (hits.Exists(h => h.Equals(list[rand])))
        rand = GameManager.instance.functions.RandomInt(list.Count - 1);
    return list[rand];
}

public static List<T> Random<T>(this IReadOnlyList<T> list, int count)
{
    List<T> result = new();
    for (int i = 0; i < count; i  )
    {
        result.Add(list.Random());
    }
    return result;
}

public static List<IEquatable> RandomUnique<IEquatable>(this IReadOnlyList<IEquatable> list, int count)
{
    List<IEquatable> result = new();
    for (int i = 0; i < count; i  )
    {
        result.Add(list.Random(result));
    }
    return result;
}

Doesn't work for strings (as in "abcdefg".Random()), but for my needs it's not neccessary.

CodePudding user response:

Instead of implementing extensions methods for List<T>, T[] etc. you can try implementing a single routine for IEnumerable<T>, e.g.

public static partial class EnumerableExtensions {
  public static T Random<T>(this IEnumerable<T> source) {
    //DONE: do not forget to validate public methods' arguments
    if (source is null)
      throw new ArgumentNullException(nameof(source));

    // If enumerable is a collection (array, list) we can address items explictitly
    if (source is ICollection<T> collection) {
      if (collection.Count <= 0)
        throw new ArgumentOutOfRangeException(nameof(source), 
          $"Empty {nameof(source)} is not supported.");

      return collection[GameManager.instance.functions.RandomInt(collection.Count - 1)];
    }

    // In general case we have to materialize the enumeration
    var list = source.ToList();

    if (list.Count <= 0)
      throw new ArgumentOutOfRangeException(nameof(source), 
        $"Empty {nameof(source)} is not supported.");

    return list[GameManager.instance.functions.RandomInt(list.Count - 1)];
  }
}

Then you can use the same extension method with list, array etc.:

// Array
int demo1 = new int[] {4, 5, 6}.Random();
// List
double demo2 = new List<double>() {1.0. 3.0}.Random(); 
// String is not array or list but implements IEnumerable<char>
char demo3 = "abcdef".Random();

CodePudding user response:

IEnumerable is specifically just a sequence of values, and has no length.

IReadOnlyList on the other hand, is a list of values (so does have a length) and does not allow adding/removing values.

A .NET array implements both.

So if you change your extension methods to take IReadOnlyList<xxx> instead of List<xxx> they should automatically work on arrays too.

CodePudding user response:

As an alternative to consider: You can use Reservoir sampling to select N items from a sequence of unknown length.

Here's a sample implementation:

/// <summary>Randomly selects n elements from a sequence of items.</summary>
public static List<T> RandomlySelectedItems<T>(IEnumerable<T> items, int n, System.Random rng)
{
    // See http://en.wikipedia.org/wiki/Reservoir_sampling for details.

    var result = new List<T>(n);
    int index = 0;

    foreach (var item in items)
    {
        if (index < n)
        {
            result.Add(item);
        }
        else
        {
            int r = rng.Next(0, index   1);

            if (r < n)
                result[r] = item;
        }

          index;
    }

    if (index < n)
        throw new ArgumentException("Input sequence too short");

    return result;
}
  • Related