An alternative to nullable in the form of a string type for the property (parameter-argument) has already been proposed here. I want to suggest a method that is considered more convenient (using the example of nullable attribute constructor arguments). We have such an attribute (in simplified form), which has 4 nullable constructor-parameters, which are the logic of three states of the bool check field:
class MyAttribute: Attribute
{
public MyAttribute(bool? a=null, bool? b=null, bool? c=null, bool? d=null) :base() { }
}
We can define this constructor in the attribute class, but we cannot use it as an attribute:
[My(null,true,d:false)] // There will be a compilation error
class Another {}
Let's make this constructor private (so as not to interfere –but we will use it) and declare a new one in such a "hack":
class MyAttribute: Attribute
{
public MyAttribute(object? a=null, object? b=null, object? c=null, object? d=null) :this((bool?)a, (bool?)a, (bool?)b, (bool?)d) { } //Here "object?" Nullable as needed (depending on the nullable setting for object types
private MyAttribute(bool? a=null, bool? b=null, bool? c=null, bool? d=null) :base() { }
}
We using nullable object type for the parameters without nullable “bool?”. And using implicit value boxing. And everything will work:
[My(null,true,d:false)] // No compiletime errors
class Another {}
There are several problems left - the first: we do not control the type of arguments that are passed to this constructor. And in our case, an explicit cast of the type - which will give an exception - if the type does not match - this is a mess!
Unfortunately, C # cannot be set to some extended compiletime contract (or in-design; a pity) to limit the actually transmitted argument type. You can manually implement a runtime contract - but what to do if it is violated - do not throw an exception? I suggest just applying your type conversion. Unfortunately, C # does not know how to override type cast operators (which is a pity). But you can create an extension method, for example, of the following type:
public static class ObjectBoolExtension
{
public static bool? ToBool(this object o)
{
switch (o)
{
case false:
case 0: return false;
case true:
case 1: return true;
default: return null;
}
}
}
Then the class will be like this
class MyAttribute: Attribute
{
public MyAttribute(object? a=null, object? b=null, object? c=null, object? d=null) :this(a.ToBool(), b.ToBool(), c.ToBool(), d.ToBool()) { }
private MyAttribute(bool? a=null, bool? b=null, bool? c=null, bool? d=null) :base() { }
}
[My(“32”,true,d:1,c:-100)] // No runtime exceptions
class Another {}
If desired, you can add diagnostics:
public static class ObjectBoolExtension
{
public static bool? ToBool(this object o,
[System.Runtime.CompilerServices.CallerMemberName] string methodName = "",
[System.Runtime.CompilerServices.CallerFilePath] string filepath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0
)
{
switch (o)
{
case false:
case 0: return false;
case true:
case 1: return true;
case -1:
case null:
return null;
default:
{
#if DEBUG
System.Diagnostics.Trace.WriteLine($"Not supported value type {o.GetType().FullName} and value \"{o.ToString()}\" from {methodName} in [{lineNumber}] {filepath}");
#endif
return null;
}
}
}
}
class MyAttribute: Attribute
{
public MyAttribute(object? a=null, object? b=null, object? c=null, object? d=null) :this(a.ToBool(nameof(MyAttribute)), b.ToBool(nameof(MyAttribute)), c.ToBool(nameof(MyAttribute)), d.ToBool(nameof(MyAttribute))) { }
private MyAttribute(bool? a=null, bool? b=null, bool? c=null, bool? d=null) :base() { }
}
But, unfortunately, trace messages appear only when this constructor is explicitly called. That I don't know how to improve. A interesting solution could be to use Roslyn processing of the code syntax - at least CodeAnalysis - such an analyzer could produce correct diagnostics. Similarly, CodeFix could allocate error locations and offer fixes on the fly. This is already the topic of implementing compiletime and in-design contracts - which goes beyond the scope of this subge.
Another problem with using the object type in the constructor-parameter is the use of reflection to create an instance of such an attribute. There may be difficulties in choosing the desired type of parameter - especially when the original values of the arguments are string (but true values of the correct literal type are needed)! I will also leave this out of this discussion.
Please don't scold me too much. This is my first post on stackoverflow ;-)