out参数不用赋值?这么神奇吗!

首先提醒大家一下,docs.microsoft.com上的《C# 指南》是这样描述out 参数修饰符[1]的:

作为 out 参数传递的变量在方法调用中传递之前不必进行初始化。但是,被调用的方法需要在返回之前赋一个值。

请注意上面加粗的话,然后看看下面的代码片段,你觉得它能否编译通过:

private void Test(out System.Reflection.ParameterModifier obj)
{ 
//什么也不做
}

如果你很肯定地回答“不能”,那么恭喜你——答错了。我当初看到这段代码的第一感觉也是不能,但发现代码确实能够编译通过。

分析原因

难道是语法改变了,官方文档没更新? 我又测试了一下:

private void Test2(out string obj)//编译失败
{ 
}
private void Test3(out int obj)//编译失败
{ 
}

难道这个类型有什么特殊之处? 我把dotnet/runtime中的ParameterModifier源代码[2]复制到本地项目,编译同样提示CS0177错误,WTF!!!

private void Test(out ParameterModifier obj)
{ 
}
public readonly struct ParameterModifier
{
    private readonly bool[] _byRef;

    public ParameterModifier(int parameterCount)
    {
        if (parameterCount <= 0)
            throw new ArgumentException();

        _byRef = new bool[parameterCount];
    }

    public bool this[int index]
    {
        get => _byRef[index];
        set => _byRef[index] = value;
    }

#if CORECLR
    internal bool[] IsByRefArray => _byRef;
#endif
}

深入Roslyn

应该是编译器做了什么特殊处理!

于是我clone了dotnet/roslyn源代码[3],本来想调试源代码的,结果由于编译时依赖包一直下载不下来,干脆直接读源代码了。

通过查找错误提示"must be assigned to before control leaves the current method",定位到CSharpResources.resx,确认错误编码为ERR_ParamUnassigned:

  <data name="ERR_ParamUnassigned" xml:space="preserve">
    <value>The out parameter '{0}' must be assigned to before control leaves the current method</value>
  </data>

查找ERR_ParamUnassigned,定位到了编译错误信息被添加的位置(DefiniteAssignment.cs文件内的ReportUnassignedOutParameter方法);

protected virtual void ReportUnassignedOutParameter(ParameterSymbol parameter, SyntaxNode node, Location location)
{
    ......
    if (Diagnostics != null && this.State.Reachable)
    {
        ......

        if (!reported)
        {
            Debug.Assert(!parameter.IsThis);
            Diagnostics.Add(ErrorCode.ERR_ParamUnassigned, location, parameter.Name);
        }
    }
}

因为同样的方法定义,只是参数类型不一样导致编译报错,因此猜测这个方法肯定进入了,只是this.State.Reachable值不同的原因,Reachable的代码如下:

public bool Reachable
{
    get
    {
        return Assigned.Capacity <= 0 || !IsAssigned(0);
    }
}
public bool IsAssigned(int slot)
{
    return /*(slot == -1) || */Assigned[slot];
}

public void Assign(int slot)
{
    if (slot == -1)
        return;
    Assigned[slot] = true;
}

继续查找Assign的调用位置,发现一段很有意思的代码:

Debug.Assert(!_emptyStructTypeCache.IsEmptyStructType(type));
......
state.Assign(slot);

IsEmptyStructType是不是意味着空Struct不检查?立马来试试:

private void Test(out EmptyStruct obj)///编译通过
{ 
}

public struct EmptyStruct
{ 
}

继续探究

但是ParameterModifier明显不是空Struct,而且更奇怪的是为什么将源代码复制到本地项目又不能编译了。 带着这个疑问,我们继续深挖:

private bool IsEmptyStructType(TypeSymbol type, ConsList<NamedTypeSymbol> typesWithMembersOfThisType)
{
    ......
    result = CheckStruct(typesWithMembersOfThisType, nts);
    ......
    return result;
}

private bool CheckStruct(ConsList<NamedTypeSymbol> typesWithMembersOfThisType, NamedTypeSymbol nts)
{
    if (!typesWithMembersOfThisType.ContainsReference(nts))
    {
        ......
        return CheckStructInstanceFields(typesWithMembersOfThisType, nts);
    }

    return true;
}
private bool CheckStructInstanceFields(ConsList<NamedTypeSymbol> typesWithMembersOfThisType, NamedTypeSymbol type)
{
    // PERF: we get members of the OriginalDefinition to not create substituted members/types 
    //       unless necessary.
    foreach (var member in type.OriginalDefinition.GetMembersUnordered())
    {
        if (member.IsStatic)
        {
            continue;
        }
        var field = GetActualField(member, type);
        if ((object)field != null)
        {
            var actualFieldType = field.Type;
            if (!IsEmptyStructType(actualFieldType, typesWithMembersOfThisType))
            {
                return false;
            }
        }
    }

    return true;
}

代码检查每个字段的类型是否是“空Struct”。这意味着如果所有实例字段都是“空Struct”,则原始类型也被视为“空Struct”,否则为“非空Struct”。看来关键就在GetActualField了:

private FieldSymbol GetActualField(Symbol member, NamedTypeSymbol type)
{
    switch (member.Kind)
    {
        case SymbolKind.Field:
            var field = (FieldSymbol)member;
            // Do not report virtual tuple fields.
            // They are additional aliases to the fields of the underlying struct or nested extensions.
            // and as such are already accounted for via the nonvirtual fields.
            if (field.IsVirtualTupleField)
            {
                return null;
            }

            return (field.IsFixedSizeBuffer || ShouldIgnoreStructField(field, field.Type)) ? null : field.AsMember(type);

        case SymbolKind.Event:
            var eventSymbol = (EventSymbol)member;
            return (!eventSymbol.HasAssociatedField || ShouldIgnoreStructField(eventSymbol, eventSymbol.Type)) ? null : eventSymbol.AssociatedField.AsMember(type);
    }

    return null;
}

private bool ShouldIgnoreStructField(Symbol member, TypeSymbol memberType)
{
    return _dev12CompilerCompatibility &&                             // when we're trying to be compatible with the native compiler, we ignore
           ((object)member.ContainingAssembly != _sourceAssembly ||   // imported fields
            member.ContainingModule.Ordinal != 0) &&                      //     (an added module is imported)
           IsIgnorableType(memberType) &&                                 // of reference type (but not type parameters, looking through arrays)
           !IsAccessibleInAssembly(member, _sourceAssembly);          // that are inaccessible to our assembly.
}

必须是Struct和代码不在同一个程序集(((object)member.ContainingAssembly != _sourceAssembly),字段类型必须是引用类型或数组(IsIgnorableType),并且是私有的(!IsAccessibleInAssembly)。我们来验证一下,将ParameterModifier源代码复制到类库中:

//ConsoleApp1.csproj
private void Test(out ClassLibrary1.ParameterModifier obj)
{
}

//ClassLibrary1.csproj
namespace ClassLibrary1
{
    public readonly struct ParameterModifier
    {
        private readonly bool[] _byRef; //编译通过
        //private readonly string _byRef; //编译通过
        //private readonly int _byRef; //编译失败
        //public readonly bool[] _byRef; //编译失败
    }
}

结论

今天我们深入了编译器的源代码分析了一个简单问题的成因:

一般来说,out参数必须在被调用方法将控制返回给调用方之前初始化。然而,编译器可以进行优化,在某些情况下,如类型是没有Public字段的Struct,将不会显示编译错误。

虽然感觉知道了也并没什么鸟用,但至少说明了好的代码风格还是非常重要的!希望这篇文章能够对你有所启发。

欢迎关注我的个人公众号”My IO“

参考资料

[1]

out 参数修饰符: https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/out-parameter-modifier

[2]

ParameterModifier源代码: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Reflection/ParameterModifier.cs

[3]

dotnet/roslyn源代码: https://github.com/dotnet/roslyn

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值