1

How can I restrict a function argument to a some specific enums? Preferably check it at compile-time (though I doubt this is possible).

What I have are 2 enums (KeyCode for keyboard keys and Mouse.Button for mouse buttons) and they are treated in the code in exactly the same way. I could simply overload the function and copy-paste the contents, but well, I'd like to avoid the nightmares.

Simplified version of what I currently have (inside a class)

enum E1 { Zero, One, Two }
enum E2 { Three, Four, Five }

// Overloads so users can only use this with enums only of type E1 or E2
public void DoEnumStuff(E1 e) {
    DoEnumStuffTemplate(e);
}
public void DoEnumStuff(E2 e) {
    DoEnumStuffTemplate(e);
}

// private function so users cannot access this generic one
private void DoEnumStuffTemplate<T>(T e) where T : struct, IConvertible {
    // check type for safety
    if (!typeof(T).IsEnum || typeof(T).Name != "E1" || typeof(T).Name != "E2")
        throw new ArgumentException();

    // do lots of stuff
    DoSomething(e); //<- overloaded function, accepts only E1 and E2 =ERROR
    // do lots of other stuff
}

For completeness sake:

  • DoSomething behaves completely different depending on which type is given
  • DoSomething is called a lot in the function
  • I cannot change the Enums
  • I do not want to change DoSomething

I think I need to be able to tell the compiler that the generic T is surely either E1 or E2, but I have no clue as of how to do this.



Edit: the situation

Lots of good suggestions, but nothing that encompasses all I want. I'll add here the code I currently have to shed some more light on the problem hopefully.

I'm making a minesweeper clone to try out Unity 2D. I've created an Action class based on the thor::ActionMap class from the library Thor in C++ used with SFML. It simply allows for neat code such as (in C++)

ActionMap actionMap<string>;
actionMap["Fire"] = Action(Keyboard::LeftControl) || Action(Mouse::Left);
// stuff
while (game.IsRunning()) {
    if (actionMap["Fire"].IsActive()) //true if left control or left mouse button is held
        // FIRE
    // probably more stuff
}

Where ActionMap is simply a dictionary of a key (here a string) and an Action. As you can see, the Action accepts both keyboard and mouse buttons which are 2 different enums. Thus the equivalent of the DoSomething(e) from the example code.

I'm now creating a method that can change the controls consistently. It uses the enum EControls as key instead of a string. Here KeyCode contains all keyboard keys and Mouse.Button all the mouse buttons. I need to differentiate between pressing and releasing of a button here, which is why both EControls.TilePressed and EControls.TileReleased will have the same key and need to be treated differently than for example EControls.GameEscape. This code is again in C#.

private ActionMap _controls = new ActionMap<EControls>();

// Set controls for a keyboard key
public void SetControl(EControls control, KeyCode key) {
    switch (control) {
        // If either TilePressed or Released was given, set them both to the same key
        case EControls.TilePressed:
        case EControls.TileReleased:
            //Here Action(...) is DoSomething(...) from the example code
            _controls[EControls.TilePressed] = new Action(key, Action.EActionType.PressOnce);
            _controls[EControls.TileReleased] = new Action(key, Action.EActionType.ReleaseOnce);
            break;
        case EControls.TileFlagPressed:
        case EControls.TileFlagReleased:
            _controls[EControls.TileFlagPressed] = new Action(key, Action.EActionType.PressOnce);
            _controls[EControls.TileFlagReleased] = new Action(key, Action.EActionType.ReleaseOnce);
            break;
        case EControls.GameEscape:
            _controls[EControls.GameEscape] = new Action(key, Action.EActionType.ReleaseOnce);
            break;
        default:
            throw new ArgumentOutOfRangeException("control");
    }
}

// Set controls for a mouse button
public void SetControl(EControls control, Mouse.Button button) {
    // copy-pasted code :(
    case EControls.TilePressed:
        case EControls.TileReleased:
            _controls[EControls.TilePressed] = new Action(button, Action.EActionType.PressOnce);
            _controls[EControls.TileReleased] = new Action(button, Action.EActionType.ReleaseOnce);
            break;
        case EControls.TileFlagPressed:
        case EControls.TileFlagReleased:
            _controls[EControls.TileFlagPressed] = new Action(button, Action.EActionType.PressOnce);
            _controls[EControls.TileFlagReleased] = new Action(button, Action.EActionType.ReleaseOnce);
            break;
        case EControls.GameEscape:
            _controls[EControls.GameEscape] = new Action(button, Action.EActionType.ReleaseOnce);
            break;
        default:
            throw new ArgumentOutOfRangeException("control");
    }
}

As you can see, in almost every line of code new Action(...) is present and code such as if (typeof(T).GetType() == typeof(E1)) is essentially the same as copy-pasting the contents of the function. Which is something I'd like to avoid (copy-pasting would even be safer on compile-time). But as it stands, it does not seem to be possible.

Since in a bigger game you'll probably regularly add some new controls, it will be quite an annoyance.

Sorry for the wall of text :s

Didii
  • 1,194
  • 12
  • 36
  • Overloading is okay I think.... That is what it is specifically designed for.. why do you think it is bad? – Ian Feb 18 '16 at 13:35
  • 2
    if `DoSomething` behaves differently for different types, why force things into a single method? Just overload and use two separate methods. – ryanyuyu Feb 18 '16 at 13:37
  • `"I'd like to avoid the nightmares"` - Nightmares such as... what, exactly? Method overloading is a pretty straightforward part of the language. Duplicated concerns can be refactored pretty easily. – David Feb 18 '16 at 13:38
  • You should consider replacing != "E1" with typeof(E1) etc. Also I would suggest to do all the common stuff in private method that receives int and call it inside each overload of DoSomething(e) and remove DoEnumStuffTemplate completely – Vitaly Feb 18 '16 at 13:47
  • I don't want the copy-pasta since the function is quite long and will need regular updates. It's the copy-paste I can get nightmares from, not from method overloading. – Didii Feb 18 '16 at 13:58
  • See [this](http://stackoverflow.com/q/5067724/1997232) and [this](http://stackoverflow.com/q/79126/1997232) regarding your last edit. – Sinatr Feb 18 '16 at 14:03
  • @Sinatr I want to restrict the types to only `E1` and `E2`, not all enums (or `struct`s/`IConvertible`s in the case you mentioned). I read about John Skeet's `UnconstrainedMelody`, and I not fully understand it, but I think this does the same. – Didii Feb 18 '16 at 15:04
  • .GetType() calls are incorrect. typeof(T).Name is all you need. – CSharpie Feb 18 '16 at 18:14
  • BTW: Seems that the `ActionMap` and `SetControl` functionality is an alternative implementation of Unity's Input.GetAxis("Vertical") stuff. – thersch Feb 19 '16 at 09:38

5 Answers5

1

I think overloading is your best bet. Factor do lots of stuff and do lots of other stuff into methods of their own, and you won't have any nightmares.

If that's really impossible, then what you have is fine. Just cast e to either E1 or E2 as appropriate. It's a little gross, but it's a private method so the ugliness shouldn't spread too far.

David Seiler
  • 9,557
  • 2
  • 31
  • 39
  • But, the `do lots of stuff` also contains lots of `DoSomething`s actually. Should have mentioned this though. And I cannot cast to `E1` or `E2` because I don't know which type they are inside the function. If I could, there shouldn't be any problem :-) – Didii Feb 18 '16 at 13:47
  • @Didii There should still be a way, maybe even passing `Action`s into sub-methods or splitting up the segments that don't use `DoSomething` better. Perhaps you should write out your code once and put it on Code Review to see what people think of your concrete case? – 31eee384 Feb 18 '16 at 15:44
  • @31eee384 I've added the code I'm using. I hope that makes things more clear of why I don't like the copy-paste and why the casting to `E1` or `E2` is essentially the same as the copy-paste. – Didii Feb 18 '16 at 19:04
1

An ugly, but a way:

private void DoEnumStuffTemplate<T>(T e) where T : struct, IConvertible
{
    if (typeof(T) == typeof(E1))
        DoEnumStuff((E1)(object)e);
    else if (typeof(T) == typeof(E2))
        DoEnumStuff((E2)(object)e);
    else
        throw new ArgumentException();
}

Make sure nobody see it.

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • This is just method overloading in disguise :p I guess I need to do it the copy-paste way :( – Didii Feb 18 '16 at 15:05
  • No no, it's **generic** method, look at its signature! ;) I only wrote it because of your comment *"I cannot cast to `E1` or `E2` because I don't know which type they are inside the function"* (and ugly check in the question). – Sinatr Feb 18 '16 at 15:08
1

You don't want to change DoSomething, but would wrapping it be ok?

private void myDoSomething(T e) where T : struct, IConvertible
{
    if (typeof(T).GetType().Name == "E1")
        DoSomething((E1)(object)e);
    else if (typeof(T).GetType().Name == "E2")
        DoSomething((E2)(object)e);
    else
        throw new ArgumentException();
}


// private function so users cannot access this generic one
private void DoEnumStuffTemplate<T>(T e) where T : struct, IConvertible {
    // check type for safety
    if (!typeof(T).IsEnum || typeof(T).GetType().Name != "E1" || typeof(T).GetType().Name != "E2")
        throw new ArgumentException();

    // do lots of stuff
    myDoSomething(e); //<- overloaded function, accepts only E1 and E2 =ERROR
    // do lots of other stuff
}
Tim Pohlmann
  • 4,140
  • 3
  • 32
  • 61
1

Here's a refactoring that resembles the Factory pattern:

public void SetControl(EControls control, Func<Action.EActionType, Action> createAction)
{
    switch (control)
    {
        case EControls.TilePressed:
        case EControls.TileReleased:
            _controls[EControls.TilePressed] = createAction(Action.EActionType.PressOnce);
            _controls[EControls.TileReleased] = createAction(Action.EActionType.ReleaseOnce);
            break;

        case EControls.TileFlagPressed:
        case EControls.TileFlagReleased:
            _controls[EControls.TileFlagPressed] = createAction(Action.EActionType.PressOnce);
            _controls[EControls.TileFlagReleased] = createAction(Action.EActionType.ReleaseOnce);
            break;

        case EControls.GameEscape:
            _controls[EControls.GameEscape] = createAction(Action.EActionType.ReleaseOnce);
            break;

        default:
            throw new ArgumentOutOfRangeException("control");
    }
}

// Call it later with:
SetControl(control, type => new Action(key, type));
SetControl(control, type => new Action(mouseButton, type));

You provide SetControl with what amounts to a partially filled constructor, createAction, that only needs the EActionType to fully create the Action.

Another way to do this (while changing more code) would be to invert the dependency: give the Action a way to set its own EActionType based on a passed in EControls.

31eee384
  • 2,748
  • 2
  • 18
  • 27
1

You need to create the Action in two steps. The caller of SetControl(...) knows whether the source is a mouse button or a key. So the caller creates the action object like new Action(key) or new Action(button).
This action object is passed to the SetControl(control, Action action) method.
SetControl knows the action type. It needs a method in Action where the action type can be set, e.g. Action.SetActionType(Action.EActionType actionType).

So the SetControl method is:

// Set controls for an action
public void SetControl(EControls control, Action action) {
    switch (control) {
        // If either TilePressed or Released was given, set them both to the same key
        case EControls.TilePressed:
        case EControls.TileReleased:
            //Here Action(...) is DoSomething(...) from the example code
            _controls[EControls.TilePressed] = action.SetActionType(Action.EActionType.PressOnce);
            _controls[EControls.TileReleased] = action.SetActionType(Action.EActionType.ReleaseOnce);
            break;
        case EControls.TileFlagPressed:
        case EControls.TileFlagReleased:
            _controls[EControls.TileFlagPressed] = action.SetActionType(Action.EActionType.PressOnce);
            _controls[EControls.TileFlagReleased] = action.SetActionType(Action.EActionType.ReleaseOnce);
            break;
        case EControls.GameEscape:
            _controls[EControls.GameEscape] = action.SetActionType(Action.EActionType.ReleaseOnce);
            break;
        default:
            throw new ArgumentOutOfRangeException("control");
    }
}

This method is called like so:

SetControl(control, new Action(key));
SetControl(control, new Action(mouseButton));
thersch
  • 1,316
  • 12
  • 19