Search code examples
c#extension-methodsrefout

I don't understand why I'm getting the CS8347 error while trying to make an extension method


I was trying to create a simple extension method wrapper around CollectionsMarshal.GetValueRefOrAddDefault(..) and I ran into a problem I don't quite understand.

This code works:

public static ref TValue? GetValueRefOrAddDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, out bool exists)
    where TKey : notnull
{
    return ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out exists);
}

If I try not to pass the out bool exists variable outside of the method by discarding it or by just storing it in a local variable, it doesn't work:

public static ref TValue? GetValueRefOrAddDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key)
    where TKey : notnull
{
    return ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out _);
    // Error: Cannot use a result of CollectionsMarshal.GetValueRefOrAddDefault(..) in this context because
    // it may expose variables referenced by parameter 'exists' outside of their declaration scope
}

To me this error sounds a lot like something I should get when trying to return a reference to a local variable. I just don't get why I'm getting the error by simply not using / passing out the boolean I received, and how could that omission possibly reference anything out of scope.

Also, the error disappears when I use a dummy variable outside of the scope of the method, but I'd rather not do that:

private static bool dummy;
public static ref TValue? GetValueRefOrAddDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key)
    where TKey : notnull
{
    return ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out dummy);
}

Solution

  • As @RichardDeeming noted in comment: the returned reference could be a reference to the out parameter, which is a local variable in the discard case.

    This issue is resolved by C#11 Low-level struct improvements # Change the behavior of out parameters

    the language will change the default ref-safe-to-escape value for out parameters to be current method. Effectively out parameters are implicitly scoped out going forward.
    ......
    The language will also no longer consider arguments passed to an out parameter to be returnable.


    Note: if you using <LangVersion> 11 or higher with pre net7 TFM, compiler will use those new rules, but reference assembly would still be annotated as RefSafetyRules(10 /*or less*/) so out var defined in GetValueRefOrAddDefault would still not be treated as scoped. You can define UnscopedRefAttribute and annotate your wrapper out var with it to resolve this issue.

    namespace System.Diagnostics.CodeAnalysis
    {
        [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
        public sealed class UnscopedRefAttribute : Attribute {}
    }
    
    public static ref TValue? GetValueRefOrAddDefault<TKey, TValue>(
      this Dictionary<TKey, TValue> dictionary, TKey key,
      [UnscopedRef] out bool exists)
      { ... }