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);
}
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. Effectivelyout
parameters are implicitlyscoped out
going forward.
......
The language will also no longer consider arguments passed to anout
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)
{ ... }