-1

I know enums can't be strings

I would like a data structure with the utility of an enum, but one which returns strings instead of integers. To be clear, I want the return type to be the enum-like type, not string. Basically, I want to be able to force a property to be usable as a string but is only allowed to be set to a value in a defined set of strings. Something like

stringenum Unit {
    Pixels = "px",
    Inches = "in"
}

class Settings {
    public Unit Unit { get; set; }
}

var settings = new Settings() { Unit = Unit.Pixels };

...

unitLabel.Text = settings.Unit;

I've seen some solutions that just create a class with properties that return a certain string. However, I need the return type to be limited to a set, not just any string.

EDIT FOR CLARIFICATION

Consider my previous example in addition to this method:

public void WriteUnit(Unit unit)
{
    Console.WriteLine(unit);
}

// Calling
WriteUnit(Unit.Pixels); // Prints "px"
WriteUnit("px"); // ARGUMENT EXCEPTION

This method will throw an ArgumentException if you pass it a string. It only accepts the type. This is specifically what I'm looking for.

E_net4
  • 27,810
  • 13
  • 101
  • 139
Collin Brittain
  • 301
  • 1
  • 3
  • 15
  • No, because the return type of those properties is "string". – Collin Brittain Aug 16 '21 at 16:34
  • Yes, that's why I asked the question. – Collin Brittain Aug 16 '21 at 16:36
  • There's no solution than for example a set of consts, fields or properties in a dedicated static class, as I know. You can't use string as enum values, only primitve integer numbers & chars are allowed: [Enumeration types (C# reference)](https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/enum) • [Enums Specification](https://docs.microsoft.com/dotnet/csharp/language-reference/language-specification/enums) • [Enum Class](https://docs.microsoft.com/dotnet/api/system.enum) • [Enum.ToUInt64](https://source.dot.net/#System.Private.CoreLib/Enum.cs,b9252aea503e83b4,references) –  Aug 16 '21 at 16:40
  • I think you could work around that, though. For example by using annotations on the enum values. – Fildor Aug 16 '21 at 16:40
  • @Fildor do you have an example? – Collin Brittain Aug 16 '21 at 16:43
  • 1
    Indeed @Fildor, maybe a [DescriptionAttribute](https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute) can match the question, a little rude. –  Aug 16 '21 at 16:44
  • 1
    Have a look at this answer: https://stackoverflow.com/a/2650090/982149 – Fildor Aug 16 '21 at 16:45
  • Actually @DavidL's answer using a dictionary as a dispatch table should fit, a good compromise. My preference would however go towards the use of an attribute, cleaner to declare I find –  Aug 16 '21 at 16:45
  • Thanks for the replies. I've considered a dictionary, but I want to be able to set a property to a type which limits it to that set of strings. – Collin Brittain Aug 16 '21 at 16:47
  • @OlivierRogier A viable solution ... with the downside that it is not obvious from the enum code alone, that you have to maintain the dictionary, too. But, well ... I'd either put them in the same file or add a comment. – Fildor Aug 16 '21 at 16:48
  • 2
    Why not use the enum member name (and ToString() it when you want a string), but the rest of the time use it as an enum? `enum Unit { px, [in] }` and `var x = Unit.px; string xs = x.ToString()` - typically you only want a string for output purposes... – Caius Jard Aug 16 '21 at 16:48
  • Consider how with an `enum` I can make a property `public Units MyUnit { get; set; }` that would then only allow children of the Units enum. I want that same behavior with string values. Returning a string isn't the issue. Does that make sense? – Collin Brittain Aug 16 '21 at 16:49
  • @CaiusJard wouldn't it just return "1" instead of 1? I'd need it to be any defined string. – Collin Brittain Aug 16 '21 at 16:49
  • @CollinBrittain no, ToString from an enum returns the enum name. – Magnetron Aug 16 '21 at 16:50
  • https://learn.microsoft.com/en-us/dotnet/api/system.enum.tostring?view=net-5.0 – Caius Jard Aug 16 '21 at 16:50
  • @Magnetron Oh I see. Can I do that in the definition or would I have to do it whenever I want the string value? – Collin Brittain Aug 16 '21 at 16:51
  • 1
    Whenever you want the string, but remember that you can often use something that calls toString anyway - for example if you Console.WriteLine'd it, it would call ToString.. If you string interpolated it it would also. You can probably boil the "number of places that call tostring on the enum" in your code down to one or two... – Caius Jard Aug 16 '21 at 16:51
  • Also, if you want your code nice and to C# conventions you could use an attribute to give a different ToString output: https://stackoverflow.com/questions/479410/enum-tostring-with-user-friendly-strings - not sure what your thoughts are on going to those lengths to be able to have e.g. `Unit.Inches` output as `"in"` – Caius Jard Aug 16 '21 at 16:56
  • @CaiusJard Thanks for the info. I think what I want is that behavior but without the need to add .ToString() everywhere that I use the enum. My feeling from all of the responses is that C# isn't really capable of doing what I want. – Collin Brittain Aug 16 '21 at 17:00
  • But that's just it - you don't ToString it "everywhere* because not everywhere can possibly *need* it as a string.. Like for example, if I make a Person class, and set their age, and pass them to another class that .. I dunno, stores them in a list and calcualtes the average ages, I don't "serialize the Person to json" just to pass it, or serialize it to store it, deser it to calcualte the age... I'm pretty sure this "I need it as a string everywhere" is actually really only "i need it as a string in my file writing routine" – Caius Jard Aug 16 '21 at 17:04
  • @CaiusJard See my edit, maybe it will make more sense. I'm not sure I'm explaining very well. – Collin Brittain Aug 16 '21 at 17:10
  • (Or put another way, you use a textbox to ask the person age, you parse it to an int, your program stores it as as an int in a db column typed as int, maths it as an int, passes it as an int.. it's whole life it's an int.. Then you write it to CSV and only then it becomes a string again, at the last moment. Every program you ever wrote got strings from the user, transformed them, kept the transformation, and only string'd them again at the final moment) – Caius Jard Aug 16 '21 at 17:11
  • I see the edit, but it's an odd problem to solve; you're the developer, and only you can cause and resolve the argument exception so maybe just "don't pass a string" (which you cannot do if it's an enum; its not a runtime argument exception but a compile time "cannot convert from string to enum" error that you'll get).. Who are you trying to control here? You say you want runtime errors as though the user will have some way of passing a string to your expects-enum method, but it seems like youre trying to control compile time errors (by other devs on the team?) – Caius Jard Aug 16 '21 at 17:14
  • @CaiusJard The point of the exception was to illustrate that the runtime respected the strongly typed behavior I want, not that I can't avoid an exception. – Collin Brittain Aug 16 '21 at 17:17
  • 1
    You could use the typing mech then... `class Unit{ public Unit Pixel { get => return PixelUnit } }` `class PixelUnit:Unit { public override string ToString() => "px"; }}` ? Should work in your WriteUnit, Console.WriteLine will call ToString().. Unit could have a string parsing routine if you need.. ? If you really wanted something to explode when a string is passed, could add an overload of WriteUnit that explodes.. but it's definitely odd to add an overload just to honeypot a developer into seeing a "don't call this method with a string".. easier to just not provide the method at all, i think – Caius Jard Aug 16 '21 at 17:19

1 Answers1

4

As mentioned in the comments, you cannot directly map an enum to a string.

That said, there is nothing preventing you from creating a map of enum to string values, that can only be accessed via the enum. If you maintain the mapping, you can guarantee that the value always exist.

public enum Unit
{
    Pixels,
    Inches
}

public static class UnitMapper
{
    private static readonly Dictionary<Unit, string> _map
        = new Dictionary<UserQuery.Unit, string>()
        {
            { Unit.Pixels, "px" },
            { Unit.Inches, "in" }
        }
        
    public static string GetUnit(Unit unit)
    {
        return _map[unit];
    }
}

Based on your additional comments, this can be combined with a custom user-defined implicit operator to give you the type of functionality you are looking for, although you will still have to call the overridden .ToString() to output a string.

public struct UnitWrapper
{
    private readonly string _unitString;
    private readonly Unit _unit;
    
    public UnitWrapper(Unit unit)
    {
        _unit = unit;
        _unitString = UnitMapper.GetUnit(_unit);
    }

    public static implicit operator UnitWrapper(Unit unit)
    {
        return new UnitWrapper(unit);
    }
    

    public override string ToString() => _unitString;
}

This can then be used as follows:

public class Settings
{
    public UnitWrapper UnitWrapper { get; set; }
}
var settings = new Settings { UnitWrapper = Unit.Pixels };
string px = settings.UnitWrapper.ToString();
David L
  • 32,885
  • 8
  • 62
  • 93
  • Thanks, David. The core problem I need solved is I want to constrain a property to a type that only allows certain string values. This would indeed allow me to get a string from a set using a method, but not define a property with a type that bounds the set of allowed strings. – Collin Brittain Aug 16 '21 at 16:46
  • I'm not sure I follow. If you override the setter of a property to expand this use case, you can always limit exactly what is allowable for any specific property. – David L Aug 16 '21 at 16:48
  • So with your answer, I couldn't give a class a property of type `Unit` that evaluates to a string. I would have to call a method to get a string based on the Unit value. I'm essentially asking, how do I make a type that behaves as though an enum could return strings instead of ints. – Collin Brittain Aug 16 '21 at 16:52
  • You _maybe_ could get something that behaves like you want with [User-defined conversion operators](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/user-defined-conversion-operators) , but it wouldn't be an enum. – Fildor Aug 16 '21 at 16:56
  • @Fildor It doesn't need to be an enum, just mimic the behavior. I'll check that out! – Collin Brittain Aug 16 '21 at 17:01
  • @CollinBrittain thanks for the additional details. I agree, a user-defined conversion operator is the right approach, with the caveat that the type will have to wrap Unit and you can't implicitly extend string, so you would have to call ToString. I've updated my answer with those details. – David L Aug 16 '21 at 17:07
  • Honestly, I don't think I'd bother with the dictionary overhead; cast it to an int and use it to index a `string[]` ? – Caius Jard Aug 16 '21 at 17:07
  • Yeah that's a bummer. I would really like to not call ToString() (see my edit in the question). I think the ultimate answer is going to be that C# is not capable of this behavior. I appreciate your help. – Collin Brittain Aug 16 '21 at 17:08
  • @CaiusJard that's a great optimization and would work as well. I was envisioning that there was more that the OP wanted to do based on the original question, but if the entire intent is to simply map, that would be a nice shortcut, it is really splitting hairs at this level. – David L Aug 16 '21 at 17:11
  • @DavidL Mapping is not what I'm looking for. I think the example in my question edit should clarify. – Collin Brittain Aug 16 '21 at 17:13
  • @CollinBrittain "Thanks, David. The core problem I need solved is I want to constrain a property to a type that only allows certain string values. This would indeed allow me to get a string from a set using a method, but not define a property with a type that bounds the set of allowed strings." Your options are: 1. Make a Runtime Check. using contains 2. Turn the Property type into a Enum - as it always should have been - and have the string interpretation of the Enum as a side project/secondary get-only property. – Christopher Aug 16 '21 at 17:19
  • @CollinBrittain if you are constraining an input/output to a range of values, the easiest way to guarantee that is a map. In addition, I am not aware of any language feature that can do specifically what you want to do. To my knowledge, this is the closest you can get. – David L Aug 16 '21 at 17:25
  • This was my shot at this: https://dotnetfiddle.net/8A9Ye4 - SO hacky ... I don't like it. – Fildor Aug 16 '21 at 17:26
  • 1
    @Fildor that just takes advantage of string format overrides. You could easily accomplish the same thing with `Console.WriteLine($"{unit}");`. In either case, it completely obfuscates intent. I agree, it is a terrible hack :). At least when explicitly calling `.ToString()`, it is obvious what you are trying to do. It's a good simplification and it avoids any sort of external map – David L Aug 16 '21 at 17:29
  • 1
    @DavidL I found myself "chasing my tail" wanting to squish all the requirements into one type ... I really don't think it's worth the hassle, too. And it's surprising the client. – Fildor Aug 16 '21 at 17:30
  • @Fildor I think it comes down to a frustration with the language for me. I've been using rust which allows enums that can evaluate to any type you want instead of just an int, and still gives you the strongly typed functionality. – Collin Brittain Aug 16 '21 at 17:34
  • Well, sometimes you chose the tool, sometimes you adapt to the features. But here's another shot: https://dotnetfiddle.net/Ocfoub - using enum and an extension method. – Fildor Aug 16 '21 at 17:44
  • ^^ Coming from Java, I also had my struggles with enum. There, this would also be easi_er_. But I learned to actually like it somehow. Forces you to code cleaner, if you are forced to keep the strings on the edges of the system. – Fildor Aug 16 '21 at 17:48