简体   繁体   中英

C#: Enums in Interfaces

I've seen a couple similar threads to this question, but none of them really answer the question I want to ask.

For starters, unfortunately I'm working with existing API code so sadly, while there may be a better way to do what I'm asking about, I'm locked in to doing it similarly to the way it is because backwards compatibility is non-negotiable.

I have a response class that currently contains an enum for an error code and a string description. The error codes define a fairly nice and complete set of responses that are all very semantically coupled to the operations where they're used.

Unfortuantely, I now have to add a different workflow for a similar set of API objects, and this will require a string description, which is fine, but also an enum error code consisting of a totally unrelated set of error codes. The error codes (and other aspects of the object model) will be used in lots of the same classes, so it would be nice to get a interface going so that i can run the objects through the same framework.

The intent here is to make a contract that says "I have an error code, and a description of that error code."

However, as far as I know there's no way to add an item to an interface such as

public interface IError
{
    enum ErrorCode;
    string Description;
}

nor is there a way to express

public interface IError<T> where T: enum
{
    T ErrorCode;
    string Description;
}

Anyone every run up against something like this before?

Yes, I've run up against this. Not in this particular situation, but in other Stack Overflow questions, like this one . (I'm not voting to close this one as a duplicate, as it's slightly different.)

It is possible to express your generic interface - just not in C#. You can do it in IL with no problems. I'm hoping the limitation may be removed in C# 5. The C# compiler actually handles the constraint perfectly correctly, as far as I've seen.

If you really want to go for this as an option, you could use code similar to that in Unconstrained Melody , a library I've got which exposes various methods with this hard-to-produce constraint. It uses IL rewriting, effectively - it's crude, but it works for UM and would probably work for you too. You'd probably want to put the interface into a separate assembly though, which would be somewhat awkward.

Of course, you could make your interface just have T : struct instead... it wouldn't be ideal, but it would at least constrain the type somewhat . So long as you could make sure it wasn't being abused, that would work reasonably well.

As Jon Skeet mentioned, the base IL supports constraining generics to be enums, however C# does not allow you to take advantage of it.

F# does allow this sort of constraint, however. Further, if the interface is defined in F#, the constraint will be enforced in C# code that implements the interface. If you're willing to mix languages in your solution, something like this should work just fine:

type IError<'T when 'T :> System.Enum and 'T : struct> =
    abstract member Error : 'T
    abstract member Description : string

If you put this in an F# project and reference it from your C# project, your C# code that implements the interface will cause a C# compiler error on any attempt to use it with a non-enum type.

You can go with your approach in a slightly different way:

public interface IError
{
    Enum ErrorCode;
    string Description;
}

System.Enum is the base class of all your enums, so that should handle it, but its far from being expressive.

The right approach here is to build your own enum classes and a base enum class for it. For eg.,

public class ErrorFlag // base enum class
{
    int value;

    ErrorFlag() 
    {

    }

    public static implicit operator ErrorFlag(int i)
    {
        return new ErrorFlag { value = i };
    }

    public bool Equals(ErrorFlag other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        return value == other.value;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as ErrorFlag);
    }

    public static bool operator ==(ErrorFlag lhs, ErrorFlag rhs)
    {
        if (ReferenceEquals(lhs, null))
            return ReferenceEquals(rhs, null);

        return lhs.Equals(rhs);
    }

    public static bool operator !=(ErrorFlag lhs, ErrorFlag rhs)
    {
        return !(lhs == rhs);
    }

    public override int GetHashCode()
    {
        return value;
    }

    public override string ToString()
    {
        return value.ToString();
    }
}

public interface IError
{
    ErrorFlag ErrorCode;
    string Description;
}

Now instead of having your own error enums, write your own ErrorFlag classes.

public sealed class ReportErrorFlag : ErrorFlag
{
    //basically your enum values
    public static readonly ErrorFlag Report1 = 1;
    public static readonly ErrorFlag Report2 = 2;
    public static readonly ErrorFlag Report3 = 3;

    ReportErrorFlag() 
    {

    }
}

public sealed class DataErrorFlag : ErrorFlag
{
    //basically your enum values
    public static readonly ErrorFlag Data1 = 1;
    public static readonly ErrorFlag Data2 = 2;
    public static readonly ErrorFlag Data3 = 3;

    DataErrorFlag() 
    {

    }
}

// etc

Now your main classes:

public class ReportError : IError
{
    // implementation
}

public class DataError : IError
{
    // implementation
}

Or otherwise,

public class ErrorFlag // base enum class
{
    internal int value { get; set; }

    public bool Equals(ErrorFlag other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        return value == other.value;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as ErrorFlag);
    }

    public static bool operator ==(ErrorFlag lhs, ErrorFlag rhs)
    {
        if (ReferenceEquals(lhs, null))
            return ReferenceEquals(rhs, null);

        return lhs.Equals(rhs);
    }

    public static bool operator !=(ErrorFlag lhs, ErrorFlag rhs)
    {
        return !(lhs == rhs);
    }

    public override int GetHashCode()
    {
        return value;
    }

    public override string ToString()
    {
        return value.ToString();
    }
}

public interface IError<T> where T : ErrorFlag
{
    T ErrorCode { get; set; }
    string Description { get; set; }
}

//enum classes
public sealed class ReportErrorFlag : ErrorFlag
{
    //basically your enum values
    public static readonly ReportErrorFlag Report1 = new ReportErrorFlag { value = 1 };
    public static readonly ReportErrorFlag Report2 = new ReportErrorFlag { value = 2 };
    public static readonly ReportErrorFlag Report3 = new ReportErrorFlag { value = 3 };

    ReportErrorFlag()
    {

    }
}

public sealed class DataErrorFlag : ErrorFlag
{
    //basically your enum values
    public static readonly DataErrorFlag Data1 = new DataErrorFlag { value = 1 };
    public static readonly DataErrorFlag Data2 = new DataErrorFlag { value = 2 };
    public static readonly DataErrorFlag Data3 = new DataErrorFlag { value = 3 };

    DataErrorFlag()
    {

    }
}

//implement the rest

To have ugly way of having enum constraints, see Anyone know a good workaround for the lack of an enum generic constraint?

The inability to write public interface IError<T> where T: enum is something that we have all complained about for years. So far there has been no progress on that.

I usually end up writing public interface IError<T> and leaving a note for the implementor that T must be an enum.

If I understand what you are wanting to do, then yes, there is no way to define an interface that contains as one of it's members a non-specific enum. Your second example is close, but you are limited to constraining the type of T to a struct , which would allow any value type. At that point, you just have to rely on the knowledge of the interfaces proper usage to let folks know the expected type of T should be an enum. You could potentially make it a little clearer by defining T as TEnum or TErrorValue :

public interface IError<TEnum> where T: struct
{
    T ErrorCode;
    string Description;
}

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