1

i have this constructor which was supposed to accept any IEnumerable of any type, and try to convert the values to the desired type

public ChartData(IEnumerable<dynamic> labels, IEnumerable<dynamic> values)
            {
                this.values = values.Select(x=>(float)Convert.ToString(x));
                this.labels = labels.Select(x=>(string)Convert.ToString(x));
            }

when i call this constructor i can pass ienumerables of the type string, but not int or float how can i make a constructor that will accept ienumerables of any type without making tons of overloads?

L4marr
  • 291
  • 2
  • 16
  • Types exist for compile time type checks. Dynamic - more so then even object - *turns off* compile time type checks. | Co- and Conravairance are a option. But usually you would make ChartData generic, so you can decide a type to put into the IEnumerable. – Christopher Aug 16 '21 at 18:24
  • Look up Generic Methods on how to do this in a proper way: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generic-methods – Christopher Aug 16 '21 at 18:26
  • 3
    If you accept any type, that would also include `IEnumerable` and ``IEnumerable`, how would you expect to convert them to float and string? It would be much better to have an object that encapsulates a float and a string value, use an interface for that. – DavidG Aug 16 '21 at 18:32
  • 1
    Something to remember here is, thanks to localization/cultural concerns, converting values to and from strings is surprisingly slow and error-prone. It's something to avoid. Therefore if we have, say, an `IEnumerable`, and need to get it to `IEnumerable`, converting each double to a string and then each of those strings back to a float is probably the slowest and least reliable possible way to handle this. – Joel Coehoorn Aug 16 '21 at 19:17

2 Answers2

3

If you accept any type, that would also include IEnumerable<User> and IEnumerable<Car>, how would you expect to convert them to float and string? It would be much better to have an object that encapsulates a float and a string value, use an interface for that. For example, have an interface like this:

public interface IChartItem
{
    float Value { get; set; }
    string Label { get; set; }
}

An example class:

public class Foo : IChartItem
{
    public int MyProperty { get; set; }
    public string Name { get; set; }
    
    public float Value => (float)MyProperty;
    public string Label => Name;
}

And now your function could be generic with a constraint:

public void ChartData<T>(IEnumerable<T> items)
    where T : IChartItem
{
    this.values = items.Select(x => x.Value);
    this.labels = items.Select(x => x.Label);
}

Alternative Option

You could also pass in your object along with a couple of Func parameters to get the value and label. For example:

public void ChartData<T>(IEnumerable<T> items, 
    Func<T, float> valueSelector, 
    Func<T, string> labelSelector)
{
    this.values = items.Select(x => valueSelector(x));
    this.labels = items.Select(x => labelSelector(x));
}

And use it like this:

ChartData(foos, f => (float)f.MyProperty, f => f.Name);
DavidG
  • 113,891
  • 12
  • 217
  • 223
2

If you don't care about the type use non-generic version IEnumerable(which is base interface for IEnumerble<T>):

public ChartData(IEnumerable labels, IEnumerable values)
{
      // x is of type Object for both lines.
      this.values = values.Select(x=>(float)Convert.ToSingle(x));
      this.labels = labels.Select(x=>(string)Convert.ToString(x));
}

Note that

  • you lose all type safety - someone can easily pass list of files or some other random types as either of arguments
  • in general generics version is preferable if you deal with value types due to boxing cost. Fortunately in your case you need boxed value anyway or Convert.

Personally I'd only accept IEnumerable<float> (or maybe double) and IEnumerable<String> as arguments - caller of this method will have more knowledge to pick desired fields/conversion than my code trying to guess the type they passed in. If some "any numeric type" would be a desired input I'd check Is there a constraint that restricts my generic method to numeric types? and implement all variant instead of falling back to runtime reflection with dynamic or lack of type safety with non-generic version.

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
  • "Personally I'd only accept `IEnumerable` and `IEnumerable` as arguments". **EXACTLY**. The caller will know the types they have, and be in a better position to handle the conversions. The `Cast<>()` and `Select<>()` methods makes this easy for them. Round-tripping everything through `string` as in the question is the slowest and least reliable way to do this. – Joel Coehoorn Aug 16 '21 at 19:19
  • 1
    @JoelCoehoorn one problem with that is leaving it open to mismatched numbers of elements. 10 values and 11 labels for example. – DavidG Aug 16 '21 at 20:46
  • 1
    @DavidG Oh, definitely. Matching collections based on index is rarely a good approach. – Joel Coehoorn Aug 16 '21 at 21:52