-1
public class Foo<T> { internal Foo() { } }
public sealed class Foo_SByte : Foo<SByte> { }
public sealed class Foo_Byte : Foo<Byte> { }
public sealed class Foo_Short : Foo<Int16> { }
public sealed class Foo_UShort : Foo<UInt16> { }
public sealed class Foo_Int : Foo<Int32> { }
public sealed class Foo_UInt : Foo<UInt32> { }
public sealed class Foo_Long : Foo<Int64> { }
public sealed class Foo_ULong : Foo<UInt64> { }
public sealed class Foo_Char : Foo<Char> { }
public sealed class Foo_Float : Foo<Single> { }
public sealed class Foo_Double : Foo<Double> { }
public sealed class Foo_Bool : Foo<Boolean> { }
public sealed class Foo_Decimal : Foo<Decimal> { }
public sealed class Foo_String: Foo<string> { }
public class Foo_Struct<T> : Foo<T> where T : struct { }
public class Foo_Enum<T> : Foo<T> where T : Enum { }

// Now T is immutable

My objective is to restrict use of Foo to all potential value types, which I am semantically (maybe incorrectly) assuming to be equivalent to saying "all immutable types"

In other words, I dont want T to be a reference type.

The implementation doesn't change between Bars. I only want the guarantee of T restricted to immutable types.

Have I missed any (excluding the simple types, which I am aware of)?

Is there a better way?

Edit: Ok, I fully implemented it. Now all uses of Foo<T> are guaranteed to be immutable. (If not, please tell me what types I missed. Or try to inherit from a Foo to prove me wrong).

NWoodsman
  • 423
  • 4
  • 10
  • 3
    There is no constraint in C# that properly expresses immutability; unfortunately that concept is not a first-class citizen of the language. You can express that you want *value* types, but mutable structs are very much a thing. As are immutable classes. Recent versions of the language do make it easier to declare immutable types in the form of records (and yes, mutable records are still a thing too), so at least callers will have an easier time producing them, but they will still be on the honor system. – Jeroen Mostert Mar 05 '22 at 19:38
  • I think your question is quite similar to this one: https://stackoverflow.com/questions/10833918/generic-method-multiple-or-type-constraint – Fruchtzwerg Mar 05 '22 at 19:43
  • @JeroenMostert I understand this, so do you agree that I indeed must write all of these empty class declarations? And BTW, structs are mutable but given a Foo with a property T Value, we can detect and handle when T changes because it is a value type. – NWoodsman Mar 05 '22 at 19:44
  • 2
    You can't detect any such thing -- what helps things along with value types is not that they're supposedly immutable (they're not) but that they have copy semantics, so you can copy an instance and not care if the original gets mutated. There is a form of class-based copy semantics in the form of `ICloneable`, but its use is not recommended due to the contractual vagueness of what `Clone` is supposed to do. Nevertheless, if all you're after is "type with copy semantics", you could (for example) mandate that the user pass a `Func Copy`, which is just a simple `return arg` for value types. – Jeroen Mostert Mar 05 '22 at 19:48
  • Agreed, it is impossible to detect mutability, and whatever you think does do that probably doesn't work. Why can you not just do `public class Foo where T : struct { }` – Charlieface Mar 05 '22 at 19:48
  • @Charlieface can't make a `Foo`, a requirement. – NWoodsman Mar 05 '22 at 19:51
  • OK what are you actually trying to achieve? You already know that `where T : immutable` doesn't work from [your other question](https://stackoverflow.com/questions/71364604/why-only-1-2-of-the-main-c-sharp-types-reference-types-is-allowed-as-a-generic), so what do you propose instead? There simply isn't any way of expressing this in C# – Charlieface Mar 05 '22 at 19:53
  • Unfortunately this is just something where you'll end up fighting the language. You can't cleanly constrain something to "primitive value types or `string`", and even if you could you would be artificially restricting things, since your type could no longer work with user-defined immutable types. Ultimately just dropping the constraint and (if you must) introduce a runtime check in the constructor on `typeof(T)` for types you find "acceptable" is probably the closest you can get, if you want the whitelist approach. – Jeroen Mostert Mar 05 '22 at 19:54
  • If you really want immutability in your .NET language, consider something like F#, which (as a functional language) is much more serious about the topic. Mutable types exist in F# too, but they are the exception rather than the default, and code must explicitly choose to mutate things with separate syntax. – Jeroen Mostert Mar 05 '22 at 20:03
  • For a concrete illustration of a type in C# where immutability is needed, but unenforceable, consider `Dictionary`. The semantics of `Dictionary` crucially depend on 1) a correct implementation of `GetHashCode` and `Equals` for `TKey` and 2) that the outcome of any call to `GetHashCode` and `Equals` does not change after an instance is used as a key. There is no way to enforce this in C# (note that value types can override the methods incorrectly, and refer to shared state) so all `Dictionary` can do is say "if you break it, you get to keep the pieces". – Jeroen Mostert Mar 05 '22 at 20:17
  • @JeroenMostert sorry Jeronen, I was using "immutability" interchangeably with "copy-type referencing". Given a property `T Foo.Value {get;set;}` `T Value` can change via assigning a new instance of T, but Value cannot otherwise mutate. – NWoodsman Mar 05 '22 at 20:29
  • @JeroenMostert They can declare the class but the constructor is protected. They can't new up instances of their class by calling the base constructor. Therefore T is still immutable. – NWoodsman Mar 05 '22 at 20:39
  • Yes, and...? A derived class has access to that constructor, so `new Foo_A()` works just fine. Fortunately, otherwise all your other `Foo_` classes wouldn't work either. The only way to prevent someone from deriving is to make the class `sealed`, but of course then your setup doesn't work anymore either. The best you could achieve here is a runtime check, in the constructor of `Foo`, as I alluded to earlier. Of course, once you have that there's no need for all the separate derived classes either anymore. – Jeroen Mostert Mar 05 '22 at 20:40
  • @Jeronen You are right, I went down a path that is wrong. – NWoodsman Mar 05 '22 at 20:44
  • 1
    `public Foo() { if (!(typeof(T).IsValueType || typeof(T) == typeof(string))) throw new ArgumentException($"{typeof(T)} does not have copy semantics."); }` is one way. The JIT appears capable of optimizing out the check when the type is instantiated, even. – Jeroen Mostert Mar 05 '22 at 20:58
  • @JeroenMostert that is how I will proceed, please answer for the upvote. – NWoodsman Mar 05 '22 at 21:08

1 Answers1

1

It isn't possible to check for immutability, neither at compile time nor at runtime. The closest you can get is detecting if a type has copy semantics (that is, if the assignment x = y will result in x having a value that is independent of y being changed -- sort of, read on), because the list of types that effectively have (intrinsic) copy semantics is restricted in C#. So you can have a runtime check for that:

class Foo<T> {
    public Foo() {
        if (!(typeof(T).IsValueType || typeof(T) == typeof(string))) {
            throw new ArgumentException($"{typeof(T)} does not have copy semantics.");
        }
    }
}

Conceptually this check happens every time a Foo<T> is instantiated, but practically speaking the JIT can optimize it away.

Note that just having copy semantics is still no guarantee of correctness with regards to state. In particular, a user-defined value type could refer to state outside the instance. Simple example:

struct SneakyStruct {
    static int i = 0;
    public int Value => i++;    
}

The fact that you can copy SneakyStruct and there's no way to modify the state from the outside is still of no help in guaranteeing you'll get the same Value every time. Of course this example is deliberately perverse, but still -- care should be taken not to rely on things you don't have to rely on, or actually can't rely on.

Jeroen Mostert
  • 27,176
  • 2
  • 52
  • 85