Search code examples
c#.netreflection

ParameterInfo.DefaultValue is null for optional value-type parameter


In the code below, when I try to get default value of an optional parameter whose type is a custom value type, I get null. This doesn't make sense to me, since null is not a valid value of type Bar.

public static void Main()
{
    var ctor = typeof(Foo).GetConstructors().First();
    foreach (var paramInfo in ctor.GetParameters())
    {
        Console.WriteLine(paramInfo);
        Console.WriteLine(paramInfo.HasDefaultValue);
        Console.WriteLine(paramInfo.DefaultValue ?? "[default value is null]");
        Console.WriteLine(paramInfo.RawDefaultValue ?? "[default value is null]");
        Console.WriteLine();
    }
}

public class Foo
{
    public Foo(int intVal1 = 3, int intVal2 = default, Bar barVal = default)
    {
    }
}

public struct Bar
{
    public string Value;

    public Bar(string value) { Value = value; }

    public Bar() : this("default constructor") { }

    public override string ToString() =>
        Value == null ? "Bar [value is null]" : $"Bar: {Value}";
}

The code outputs the following:

Int32 intVal1
True
3
3

Int32 intVal2
True
0
0

Bar barVal
True
[default value is null]
[default value is null]

Instead of "[default value is null]", I would have expected "Bar [value is null]".

First of all, is this a bug in .NET? Secondly, how can I get the default value? The code below seems to work for this case, but I don't know whether to trust it because I don't know whether my assumption of paramInfo.HasDefaultValue && paramInfo.ParameterType.IsValueType && paramInfo.DefaultValue == null covers all the cases where I need to use GetDefaultValueOfType.

public static object? GetDefaultParameterValue(ParameterInfo paramInfo)
{
    if (paramInfo.HasDefaultValue)
    {
        var defaultValue = paramInfo.DefaultValue;
        if (paramInfo.ParameterType.IsValueType && defaultValue == null)
        {
            // Seems to happen for custom structs.
            // I don't know why this happens; is it a bug in .NET?
        }
        else
        {
            return defaultValue;
        }
    }

    return GetDefaultValueOfType(paramInfo.ParameterType);
}

public static object? GetDefaultValueOfType(Type type)
{
    if (type.IsValueType)
    {
        return typeof(ParameterVM)
            .GetMethod(nameof(GetDefaultValueOfTypeGeneric))
            .MakeGenericMethod(type)
            .Invoke(null, null);
    }
    return null;
}

public static T GetDefaultValueOfTypeGeneric<T>()
{
    return default;
}

Note that the answers on Getting DefaultValue for optional Guid through reflection? are not applicable here. Activator.CreateInstance() uses the default constructor, which results in the output "Bar: default constructor", whereas actually constructing a Foo and printing barVal results in "Bar [value is null]".


Solution

  • First of all, is this a bug in .NET?

    It is not a bug. It is (somewhat) specified in the ECMA-335 standard.

    II.22.9 Constants

    The Constant table is used to store compile-time, constant values for fields, parameters, and properties.
    The Constant table has the following columns:

    • Type (a 1-byte constant, followed by a 1-byte padding zero); see §II.23.1.16 . The encoding of Type for the nullref value for FieldInit in ilasm (§II.16.2) is ELEMENT_TYPE_CLASS with a Value of a 4-byte zero. Unlike uses of ELEMENT_TYPE_CLASS in signatures, this one is not followed by a type token.
    • ...

    Type shall be exactly one of:

    • ELEMENT_TYPE_BOOLEAN, ELEMENT_TYPE_CHAR, ELEMENT_TYPE_I1, ELEMENT_TYPE_U1, ELEMENT_TYPE_I2, ELEMENT_TYPE_U2, ELEMENT_TYPE_I4, ELEMENT_TYPE_U4, ELEMENT_TYPE_I8, ELEMENT_TYPE_U8, ELEMENT_TYPE_R4, ELEMENT_TYPE_R8,
    • or ELEMENT_TYPE_STRING;
    • or ELEMENT_TYPE_CLASS with a Value of zero (§II.23.1.16)

    The confusing part is implicitly reusing the ELEMENT_TYPE_CLASS for both reference and custom value types (which have their own element constant ELEMENT_TYPE_VALUETYPE).

    Possible reason for this is that the default value for a struct is always all if its field zeroed out. If we had a large struct with default value explicitly written in the metadata, we would have to waste a lot of space just for 0s that convey no real information - neither to reflection users nor to the runtime.

    Secondly, how can I get the default value?

    You can add this if you are in reflection context:

    if (paramInfo.ParameterType.IsValueType && paramInfo.HasDefaultValue) {
        var instance = Activator.CreateInstance(paramInfo.ParameterType);
        Console.WriteLine(instance);
    }
    

    EDIT: As mentioned in your comment the Activator.CreateInstance will call the default parameterless constructor which will yield a different value than the default one in your case.

    One way I can think of to get the value when we just know the Type would be to write an extension method (EDIT: included a lot of edge cases from a variety of answers to this question):

    public static class TypeExtensions {
        private static MethodInfo s_getDefaultTypeGeneric =
            typeof(TypeExtensions).GetMethod(
                nameof(TypeExtensions.GetDefaultTypeValueCore),
                BindingFlags.Static | BindingFlags.NonPublic);
    
        private static ConcurrentDictionary<Type, object> s_defaultValues = new();
        private static T GetDefaultTypeValueCore<T>() => default(T);
    
        public static object GetDefaultTypeValue(this Type type) {
            ArgumentNullException.ThrowIfNull(type);
            if (type.ContainsGenericParameters)
                throw new InvalidOperationException($"No default type value for open generic type {type.Name}");
            if (type == typeof(void))
                throw new InvalidOperationException("No default type value for System.Void");
    
            if (!type.IsValueType) return null;
    
            var defaultValue = s_defaultValues.GetOrAdd(type,
                t => s_getDefaultTypeGeneric.MakeGenericMethod(t).Invoke(null, null));
            return defaultValue;
        }
    }
    

    General Use:

    var defaultValue = typeof(Bar).GetDefaultTypeValue();
    

    in our reflection-context:

    if (paramInfo.ParameterType.IsValueType && paramInfo.HasDefaultValue) {
        var instance = paramInfo.ParameterType.GetDefaultTypeValue();
        Console.WriteLine(instance);
    }