简体   繁体   中英

Cast to object with generic type when type is unkown

I have a set of Option objects for a settings screen for my game. The Option class has a generic type that is different for each of the child classes.

The classes currently look like the following:

    public interface IOption
    {
        string GetName();

        void SetName(string name);
    }
    //Note that this does not inherit from the Option class
    public class ExitOption : IOption
    {
        private string name;

        public ExitOption(string name) => this.name = name;

        public string GetName()
        {
            return name;
        }

        public void SetName(string name)
        {
            this.name = name;
        }
    }
    public class Option<T> : IOption
    {
        public string Name;
        private T value;
        private T defaultValue;
        public T[] Values;

        public Option(string name, T defaultValue, params T[] values)
        {
            (Name, value, this.defaultValue, Values) = (name, defaultValue, defaultValue, values);
        }

        public T GetValue()
        {
            return value;
        }

        public void SetValue(T value)
        {
            this.value = value;
        }

        public string GetName()
        {
            return Name;
        }

        public void SetName(string name)
        {
            this.Name = name;
        }
    }
    public class IntegerOption : Option<int>
    {
        private int minValue;
        private int maxValue;

        //                                                                                                         Creates an array of numbers between the min and max value
        public IntegerOption(string name, int defaultValue, int minValue, int maxValue) : base(name, defaultValue, RangeCreator.IntegerRange(minValue, maxValue))
        { }
    }
    //"Key" is an enum
    public class KeyOption : Option<Key>
    {
        //                                                                         Creates an array containing all enum constants
        public KeyOption(string name, Key defaultValue) : base(name, defaultValue, RangeCreator.EnumRange<Key>())
        {}
    }
}

I constructed the objects like this:

ExitOption exit = new ExitOption("Exit");
IntegerOption volume = new IntegerOption("Volume", 100, 0, 100);
KeyOption jump = new KeyOption("Jump", Key.Spacebar);

And put them in a list:

List<IOption> options = new List<IOption>();

options.Add(exit);
options.Add(volume);
options.Add(jump);

The problem arises when I, for example, want to iterate through all options and change their value to the last one in their range, or do any sort of value change. In java I would do the following:

    for(IOption option : options)
    {
        if(option instanceof ExitOption)
        {
            //Handle exiting the menu
        }
        else
        {
            //Type is unkown, therefore I do not provide any type arguments
            Option currentOption = (Option) option;
            currentOption.SetValue(currentOption.Values[currentOption.Values.length - 1]);
        }
    }

How would I accomplish a similar thing in C#?

Here's the "ugly base class" version and a couple of C#-ifying changes as well:

using System;
using System.Collections.Generic;
using System.Linq;

public interface IOption
{
    string Name { get; set; }
}
public class ExitOption : IOption
{
    public ExitOption(string name) => Name = name;
    public string Name { get; set; }
}
public abstract class Option : IOption
{
    public Option(string name) => Name = name;
    public string Name { get; set; }
    public abstract object ObjValue { get; set; }
  public abstract IEnumerable<object> ObjValues { get;}
}
public class Option<T> : Option
{
    private T _value;
    private List<T> _values;

    public Option(string name, T initialValue, params T[] values) : base(name)
    {
        (_value, _values) = (initialValue, values.ToList());
    }
    public override IEnumerable<object> ObjValues { get => _values.Cast<object>().AsEnumerable(); }
    public T Value { get => _value; set => _value = value; }
    public override object ObjValue { get => _value; set => _value = (T)value; }
    public IEnumerable<T> Values => _values.AsEnumerable();
}

When you want/need to work without having a generic parameter, you use the Option type and its ObjXxx properties to access its values. However, when you do have the generic type parameter available, Option<T> is still the preferred means of accessing this data.

I'm still slightly dubious of Name being mutable...

While avoiding commenting on the class design chosen, you could add a method to IOption which does exactly what you want. This is, in fact, the purpose of interfaces after all - to hide such details from consumers. If all you want to do is update the value to the last value in the range, then doing something like the following should support that:

public interface IOption
{
    string GetName();
    void SetName(string name);
    // or some other appropriate name for this
    void SetValueToLastInRange();
}

It seems that the function itself doesn't require input from the call site. Perhaps it could have a much better name to encompass the exit behavior as well.

EDIT: I think it's important to emphasize that this behavior should not be decided from the call site. The data is held within the IOption implementation. You're taking it out of IOption just to pass it right back into IOption -- why even expose the data at all? In general I find it reduces coupling to limit the data exposed and instead expose the behaviors. You already have the interface set up for this, all you're missing is a new method on IOption that does what you need it to do - and then each implementation can do it their own special way (which it seems is the same for every implementation exception exit).

Create additional interface that has object instances of value/values

public interface IValueOption : IOption
{
    void SetValue(object value);
    object GetValue();
    object[] Values { get; }
}

Then your options implement this interface:

public class Option<T> : IOption, IValueOption
{
    object[] IValueOption.Values => Values.Cast<object>().ToArray();
    void IValueOption.SetValue(object value)
    {
        SetValue((T)value);
    }
    object IValueOption.GetValue()
    {
        return GetValue();
    }
   ...
}

Ofcourse this will not let you change Values array, but you can use them and call SetValue/GetValue.

foreach(IOption option in options)
{
    if(option is ExitOption)
    {
        //Handle exiting the menu
    }
    else if(option is IValueOption)
    {
        //Type is unkown, therefore I do not provide any type arguments
        IValueOption currentOption = (IValueOption)option;
        currentOption.SetValue(currentOption.Values[currentOption.Values.Length - 1]);
    }
}

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