Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API proposal: Nullable.RefValue, RefValueOrDefault #1534

Closed
nagya opened this issue Jan 9, 2020 · 14 comments · Fixed by #64677
Closed

API proposal: Nullable.RefValue, RefValueOrDefault #1534

nagya opened this issue Jan 9, 2020 · 14 comments · Fixed by #64677
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime
Milestone

Comments

@nagya
Copy link

nagya commented Jan 9, 2020

EDIT: @stephentoub's proposal update from #1534 (comment) on 1/29/2022:

namespace System
{
    public static class Nullable
    {
+        public static ref readonly T GetValueRefOrDefaultRef<T>(in T? nullable) where T : struct => ref nullable.value;
    }
}

Proposed API

namespace System
{
    public static class Nullable
    {
        public static ref readonly T RefValue<T>(this in T? n) where T : struct;
        public static ref readonly T RefValueOrDefault<T>(this in T? n) where T : struct;
        public static ref readonly T RefValueOrDefault<T>(this in T? n, in T defaultValue) where T : struct;
    }
}

These parallel the Value property and the GetValueOrDefault methods of Nullable<T>, but return the value by reference. They're extension methods because of CS8170: Struct members cannot return 'this' or other instance members by reference.

They're useful for accessing the value wrapped by a Nullable<T> without incurring a copy.

Reference implementation

namespace System
{
    public static class Nullable
    {
        public static ref readonly T RefValue<T>(this in T? n) where T : struct
        {
            if (n.HasValue)
                return ref n.value;
            else
                throw new InvalidOperationException();
        }

        public static ref readonly T RefValueOrDefault<T>(this in T? n) where T : struct
        {
            return ref n.value;
        }
        
        public static ref readonly T RefValueOrDefault<T>(this in T? n, in T defaultValue) where T : struct
        {
            return ref n.HasValue ? ref n.value : ref defaultValue;
        }
    }
}
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Jan 9, 2020
@nagya
Copy link
Author

nagya commented Jan 9, 2020

To ensure that the implementation does not to incur any copies either, I believe the HasValue property would need to be marked readonly, so the call to it wouldn't defensively copy the in parameter. Or its backing field be made internal and used directly instead.

@joperezr joperezr added api-suggestion Early API idea and discussion, it is NOT ready for implementation and removed untriaged New issue has not been triaged by the area owner labels Jul 7, 2020
@joperezr joperezr added this to the Future milestone Jul 7, 2020
@GrabYourPitchforks
Copy link
Member

What if we put this on the existing MemoryMarshal class?

public static class MemoryMarshal
{
    public static ref T GetNullableValueRef(ref Nullable<T> value);
}

It just returns a ref to the underlying _value field. No checks are performed.

If we put it on that type, it would be one of the few completely type-safe APIs on that class. It's type-safe because even though the value of any given Nullable<T> cannot be publicly set once the Nullable<T> is created, if you had a mutable ref to a Nullable<T> you could've overwritten the entire thing anyway.

@nagya
Copy link
Author

nagya commented Dec 11, 2020

Sounds good. If needed, all the other variants could be easily implemented using GetNullableValueRef.

@GrabYourPitchforks
Copy link
Member

I should correct my earlier statement: it wouldn't be completely type-safe because it would allow creation of a "null" Nullable<T> wrapped around a non-default T, but whatever. MemoryMarshal isn't intended to be safe anyway.

@stephentoub
Copy link
Member

stephentoub commented Jan 29, 2022

I suggest we simply do:

namespace System
{
    public static class Nullable
    {
+        public static ref readonly T GetValueRefOrDefaultRef<T>(in T? nullable) where T : struct => ref nullable.value;
    }
}

Everything else can be built on top of that: if you want the unsafety of a ref T instead of a ref readonly T, you can compose this with Unsafe.AsRef, and if you want a null ref or a ref to something else if !HasValue, you can write that yourself... this method simply provides a ref readonly to the underlying field as a building block for whatever else you may need.

For naming, it mirrors both Nullable<T>.GetValueOrDefault and CollectionsMarshal.GetValueRefOrNullRef in structure.

Putting it as a static on Nullable instead of on Nullable<T> makes it pay-for-play from an AOT perspective. Debatable whether it should be an extension method, but I'd argue it's a rare enough need that it shouldn't clutter up the IntelliSense for T? and should be left as a non-extension, and we already have the non-generic System.Nullable for other static helpers around Nullable<T>.

@stephentoub stephentoub added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Jan 29, 2022
@stephentoub stephentoub modified the milestones: Future, 7.0.0 Jan 29, 2022
@rickbrew
Copy link
Contributor

cc @Sergio0694 re: #64491

@Sergio0694
Copy link
Contributor

Ah, well I swear this issue hadn't showed up when I tried searching before opening #64491, I'm sorry for the duplicate 😅

The new proposal Stephen mentioned seems perfect, it's all we need anyway to then build on top of it.
Happy to help implementing this if it's marked up for grabs once it's approved 👍

@GrabYourPitchforks
Copy link
Member

Taking this via in might lead to unexpected results due to unintentionally allowing rvalues to be captured. See related discussion in #31170, where we opted to take a parameter as ref to avoid this possibility, even though we weren't mutating the value.

@stephentoub
Copy link
Member

Taking this via in might lead to unexpected results due to unintentionally allowing rvalues to be captured.

Can you share an example?

@GrabYourPitchforks
Copy link
Member

Can you share an example?

class SomeClass
{
    public int? SomeProperty { get; set; }
}

SomeClass c = new SomeClass() { SomeProperty = 42 };
ref readonly int theRef = ref Nullable.GetValueRef(c.SomeProperty);
c.SomeProperty = 84;
Console.WriteLine(theRef); // "42"

In #31170 there was a proposal for a "thou shalt not pass rvalues to this API" analyzer. That didn't really go anywhere at the time. But presumably that analyzer would be a way to bridge the pit of failure if we decide that such a pit really does exist here.

@terrajobst
Copy link
Member

terrajobst commented Feb 1, 2022

Video

  • This API might open a pit of failure where the compiler made a copy before passing in the value, thus exposing a ref to the copy, not the original
    • Example: Nullable.GetValueRefOrDefaultRef(someObj.SomeProp)
    • We could add an analyzer but it might be very difficult to write an analyzer that fully understands all the places where the compiler makes a copy. However, we might be able to construct an analyzer to checks for known-safe cases, e.g. when passing in locals or fields and warns on everything else or on known unsafe cases. Either way, the analyzer could add value without having to duplicate the logic of the compiler. Another option is force the call site to specify in, which is a safe pattern. However, we don't want to add a global analyzer because that would result in us pushing for a given language use or code style and we don't believe that's our role. We could, however, have targeted analyzer for specific APIs. This is something where having a conversion with @dotnet/LDM would be a good idea.
namespace System
{
    public static partial class Nullable
    {
        public static ref readonly T GetValueRefOrDefaultRef<T>(in T? nullable) where T : struct;
    }
}

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Feb 1, 2022
@jaredpar
Copy link
Member

jaredpar commented Feb 1, 2022

Another option is force the call site to specify in, which is a safe pattern

That seems to be solving a specific problem though. Consider that a similar problem exists when in is used in an extension method this parameter. In that case there is no place to insert an in without undoing the extension part. Also ref has endless silent copies associated with it for struct instance methods.

I think a more encompassing feature might be something like an attribute along the lines of [RequiresLocation]. This could be applied to struct instance methods or in / ref / out parameters. The impact would be at the callsite the matching argument must be a location.

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Feb 2, 2022
@GSPP
Copy link

GSPP commented Feb 7, 2022

With respect to naming, I think that GetValueRefOrDefaultRef is not correct when taken literally. It does not return a ref to a default value. It returns a ref to a location that currently happens to hold a default value, but that might change.

Ideas:

  • GetValueRef
  • GetValueStorageRef
  • GetInternalValueRef //I like this one because it sounds like an unsafe construct, which it is.

@stephentoub
Copy link
Member

It returns a ref to a location that currently happens to hold a default value, but that might change.

This was all discussed in the API review meeting and the proposed name was still considered the least bad option.

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Feb 7, 2022
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Feb 7, 2022
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Feb 17, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Mar 19, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime
Projects
None yet
Development

Successfully merging a pull request may close this issue.