dotnet / csharplang

The official repo for the design of the C# programming language
11.6k stars 1.03k forks source link

Idea : Property References #1677

Open AlgorithmsAreCool opened 6 years ago

AlgorithmsAreCool commented 6 years ago

A while back I had a bad idea. But I think there was something useful in the concept.

Today I had some code that looked like this:

Existing code

public static void NormalizeDocument(XDocument xdoc)
{
    //normalize names to lowercase
    foreach (var element in xdoc.Descendants())
    {
        element.Name = XName.Get(element.Name.LocalName.ToLower(), element.Name.Namespace.NamespaceName);
    }
}

private static XName Normalize(XName xName) => XName.Get(xName.LocalName.ToLower(), xName.Namespace.NamespaceName);

It occurred to me that if Name was a field I could just write a ref version like this.

So why not allow this convenience?

Proposed code

public static void NormalizeDocument(XDocument xdoc)
{
    //normalize names to lowercase
    foreach (var element in xdoc.Descendants())
    {
        Normalize(refprop element.Name);
    }
}

private static void Normalize(refprop XName name) => name = XName.Get(name.LocalName.ToLower(), name.Namespace.NamespaceName);

The idea being that the compiler gives us a struct similar to the following, but the expression is an lValue just like a ref field.

public readonly struct PropertyReference<T>
{
    public readonly Func<T> Get;
    public readonly Action<T> Set;
}

A few thoughts

An open question is should there be a way to access the underlying property name?

Good Idea/Bad Idea?

ufcpp commented 6 years ago

https://github.com/dotnet/csharplang/issues/1385 https://github.com/dotnet/csharplang/issues/84

HaloFour commented 6 years ago

The C# compiler can do this without any ceremony, just like the VB.NET compiler does:

Module XmlHelpers
    Public Sub NormalizeDocument(ByVal doc As XDocument)
        For Each element In doc.Descendants()
        Normalize(element.Name)
        Next element
    End Sub

    Public Sub Normalize(ByRef name As XName)
        name = XName.Get(name.LocalName.ToLower(), name.NamespaceName.ToLower())
    End Sub
End Module

What VB.NET does is capture the value of the property into a local, pass that by ref and then assign that local back to the property. It's clean and works pretty well, but it doesn't reflect the changes made immediately by the callee until after that method returns successfully.

theunrepentantgeek commented 6 years ago

There's no reason that this should require new keywords. The existing ref and out keywords would work just fine (they specify intent, not implementation).

As @HaloFour pointed out VB.Net already allows this, and the same approach (of creating a local variable) could be used - if this was something that C# wanted to do.

The code would look like this:

public static void NormalizeDocument(XDocument xdoc)
{
    // normalize names to lowercase
    foreach (var element in xdoc.Descendants())
    {
        Normalize(ref element.Name);
    }
}

private static void Normalize(ref XName name) 
    => name = XName.Get(name.LocalName.ToLower(), name.Namespace.NamespaceName);

and the compiler would convert the syntactic sugar into this:

public static void NormalizeDocument(XDocument xdoc)
{
    // normalize names to lowercase
    foreach (var element in xdoc.Descendants())
    {
        var name = element.Name;
        Normalize(ref name);
        element.Name = name;
    }
}

But ... I remember this being intensely discussed back when C# was a brand new language and I believe there were some pretty compelling reasons why the C# designers decided not to support this. Unfortunately, I can't remember the details and I haven't been able to find any references with a quick search.

I guess it's possible that opinions will have changed in the intervening time.

scalablecory commented 6 years ago

There's no reason that this should require new keywords. The existing ref and out keywords would work just fine (they specify intent, not implementation).

Last time this was asked for, I pointed out that allowing ref and out on properties would majorly break thread-safe code. I think it could be useful but a new keyword should be required to prevent such mistakes.

HaloFour commented 6 years ago

@scalablecory

properties would majorly break thread-safe code.

Only if the expectation is that the property would be written to immediately. I can't imagine that there's much code that has that expectation and I would question any label of "thread-safe" applied to that code. I doubt most people realize that ref parameters are written immediately, let alone rely on it.

xZise commented 4 years ago

I had a similar idea (#3084), but a bit expanded to allow read only and write only properties. Copied from there and renamed with the scheme used here:

interface IReadPropertyReference<T>
{
    T Get();
    T Value { get; }  // Alternative
}

interface IWritePropertyReference<T>
{
    void Set(T value);
    T Value { set; }  // Alternative
}

public sealed class PropertyReference<T> : IReadPropertyReference<T>, IWritePropertyReference<T>
{
}
TahirAhmadov commented 2 years ago

Related idea - it would be nice to create delegates which bind directly to the property getter without using reflection:

class Class
{
int Prop { get; set; }
}

void Foo(Func<int> get, Action<int> set) { }

var c = new Class();
Foo(c.Prop, c.Prop); // something like that - I'm sure a better (more explicit) syntax can be devised
svick commented 2 years ago

@TahirAhmadov That's covered by https://github.com/dotnet/csharplang/discussions/84.

NagayamaToshiaki commented 2 years ago

I would love this feature to be used in local variables, because writing same class and name is tedious.

public class GameDetailData
{
    public int Year { get; set; }
}
// Currently, when you need to update the value for GameDetailData.Year, you need to write this.
// I know this sample code is bad, but hey.
string nintendo = GetGameName(args);
switch (nintendo)
{
    case "Super Mario Odyssey":
        GameDetailData.Year = 2017;
        break;
    case "Monster Hunter Rise":
        GameDetailData.Year = 2021;
        break;
   // And so on...
}

// If property can be referrable, you can write this method like this:
ref var year = ref GameDetailData.Year;
string nintendo = GetGameName(args);
switch (nintendo)
{
    case "Super Mario Odyssey":
        year = 2017;
        break;
    case "Monster Hunter Rise":
        year = 2021;
        break;
   // And so on...
}
333fred commented 2 years ago

You can already do

string nintendo = GetGameName(args);
GameDetailData.Year = nintendo switch
{
    "Super Mario Odyssey" => 2017, 
    "Monster Hunter Rise" => 2021,
   // And so on...
}