iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
📝

[C#] Properties Revisited: More Than Just Getters and Setters

に公開1

Introduction: Do you know about C# "Properties"?

Does everyone know what a C# "Property" is?

"I know!
You mean those getters/setters with the special syntax, right?"

...For those who answered that way.

This is **not wrong, but it's also wrong!!! (?)

The role of "Properties" in modern C# is broad and important

To be sure, in introductory C# articles, they are explained like this:

Properties are getters/setters that look like member variables from the outside.
— An introductory C# guide somewhere

As a functional explanation, it's not incorrect.

However, while this explanation isn't wrong, it is insufficient.

Insufficient explanation...!

Properties in modern C# are

  • much broader in usage
  • much higher in importance

than the scope you would imagine from that explanation!!!

So, let's have a "Re-introduction" to the world of properties in modern C#.

When we talk about properties, we first mean auto-implemented properties

The declaration for a C# "property" uses this syntax:

How to define a property
public int IntProp { get; set; }

It's very simple.
It's almost no different from a "field" (what other languages call a member variable) except for adding { get; set; } at the end.

Reference: Comparison with a field (= member variable)
public int IntField;

Also, on the usage side, properties and fields are written exactly the same way.

No difference in access method
class A{
  public int IntProp { get; set; }  // I'm a property!
  public int IntField;              // And I'm a field!

  public void Clear()
  {
    // Access from within is the same
    IntProp = default;
    IntField = default;
  }
}

// Access from outside is also the same
var a = new A();
a.IntProp = 123;
a.IntField = 456;

And if you use them like this, you don't really need to be conscious of getters/setters.

Because there are no getter/setter-like elements!
For normal use, it's enough to think of them as variables attached to a class or struct!

I'll write about the reasons in detail later, but in modern C#, properties are often "used as they are".

Official Recommendation: Use Properties instead of Fields for "Public" Members

There is an important point regarding the choice between properties and fields.

[Deprecated] ↓ This will be flagged ↓ [CA1051]
public int IntField;

The example I showed earlier, ↑ this ↑ (a public field). If the official Analyzer (= Linter) is enabled[1], you will be flagged.

Official Analyzer

CA1051: Do not declare visible instance fields (code analysis) - .NET | Microsoft Learn

The official recommendation is that anything that looks like a member variable and is visible from the outside should be a property.

  • Reasons:
    • The effort to write a property is the same as a field.
    • Fields are weak against specification changes.

Simply put, fields are inconvenient and can be dangerous in some cases. In practice, properties are incredibly important in modern C#.

Properties are Resilient to Specification Changes

The statement "fields are weak against specification changes" means that properties are more resilient to specification changes compared to fields! After all, you can change the internal implementation without having to change the calling side!

class A{
  int _intProp;
  public int IntProp
  {
    get => _intProp;
    set
    {
      // Due to a specification change, we now record double the value!
      _intProp = value * 2;
    }
  }
}

var a = new A();
// No change needed here
a.IntProp = 123;
Console.WriteLine(a.IntProp);	//output: 246

Now we finally see some getter/setter nature...!

While it's ideal not to have specification changes, you can't always guarantee that you can change the calling side... Properties are strong in case of emergencies.

Thinking of them as "class member variables that you can manage later" is reassuring! While their everyday usability is just as practical as what other languages call "class member variables," properties are quite robust when you inevitably need to change the implementation.

Safe and Secure Properties

Properties Can Be Made Secure

Properties allow for various access control options compared to fields, so (if utilized properly) your code becomes more secure.

// init accessor: achieves immutability
public int IntProp { get; init; }

// Read-only with a default value specified
public int IntProp2 { get; } = 10;

// Read-only from outside, writable from within
public int IntProp3 { get; private set; }

// Required property: must be defined during initialization
public required string StringProp { get; set; }

// Combination: Required initialization and immutable
public required int IntProp4 { get; init; }

// Set-only properties cannot be made as auto-implemented properties, but are still possible
int _setOnlyProp;
public int SetOnlyProp { set => _setOnlyProp = value; }

In particular, init and required can make your code more secure. Moreover, because they require minimal code, you can use them without being overly conscious of getters/setters.

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/init

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/required-members

Immutability in "record" types is also based on init-only properties

By the way, the primary constructor of a record type internally creates properties with init accessors.

// record's primary constructor
record Person(string Name, DateTime Birthday);

// Internally, this is what's created
class Person
{
    public string Name { get; init; }
    public DateTime Birthday { get; init; }

    public Person(string Name, DateTime Birthday)
    {
        this.Name = Name;
        this.Birthday = Birthday;
    }

    public void Deconstruct(out string Name, out DateTime Birthday)
    {
        Name = this.Name;
        Birthday = this.Birthday;
    }
}

https://ufcpp.net/study/csharp/datatype/record/#primary-constructor

In modern C#, when we talk about safe and immutable data types, we talk about record, so properties contribute significantly to the safety of records.

Use Case for Properties: Data Binding in the MVVM Pattern

In standard .NET UI frameworks over the years (such as MAUI and WPF), the concept of the MVVM pattern has become widespread[2].

These UI frameworks use an XML-based format called XAML for the View. By performing data binding with the properties of a ViewModel class, the system ensures that changes to a property are immediately reflected in the appearance.

https://en.wikipedia.org/wiki/Data_binding

In older MVVM libraries, you had to write cluttered boilerplate code within properties. However, in recent, modern MVVM libraries, properties remain almost exactly like simple auto-implemented properties.

XAML
<TextBlock Text="{Binding Name}" />
The data binding target in a ViewModel is a property
// For CommunityToolkit.Mvvm v8.4+
[INotifyPropertyChanged]
public partial class MyViewModel{
  [ObservableProperty]
  public partial string Name { get; set; }
}

// For Epoxy
[ViewModel]
public sealed class MyViewModel{
  public string? Name { get; set; }
}

In other words, properties are used extensively in the MVVM pattern for C# UI programming, and nowadays, they are hardly even thought of as getters/setters.

Looking at this, you can specifically see why the explanation that "properties are just getters/setters" is slightly off or insufficient, at least in the context of C# UI programming.

Use Case for Properties: Hook Points for Source Generators

In modern C#, there is a mechanism called "Source Generators" (SG) that automatically generates code during compilation to minimize boilerplate.

https://learn.microsoft.com/en-us/shows/on-dotnet/c-source-generators

SGs are crucial for "NativeAOT" (native binary output) support, so almost all new .NET libraries highlight their NativeAOT compatibility by utilizing SGs.

https://zenn.dev/inuinu/articles/csharp-native-aot

And the key to these SGs is "Properties." An approach we've been seeing frequently lately is using properties as the hook point for SGs to automatically generate implementations.

In C# 13.0, it became possible to define partial properties. This allows you to create a property as a hook point for an SG to implement automatically...!

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-13.0/partial-properties

partial class C
{
  // Defining declaration
  public partial string Prop { get; set; }
}

Actually, the previous CommunityToolkit.Mvvm example was also an SG for properties. Like GeneratedRegex, the number of heavily used SG mechanisms that find properties to be the ideal target will likely continue to increase...

Properties are going to remain incredibly important!

Future Properties: the field Keyword

Among the examples provided so far, there are actually cases that will become even easier to write in the upcoming C# 14.0.

Examples of properties resilient to specification changes
class A{
  int _intProp;
  public int IntProp
  {
    get => _intProp;
    set
    {
      // Due to a specification change, we now record double the value!
      _intProp = value * 2;
    }
  }
}
Example of defining a set-only property
// Set-only properties cannot be made as auto-implemented properties, but are still possible
int _setOnlyProp;
public int SetOnlyProp { set => _setOnlyProp = value; }

These two will be able to be written as follows:

New syntax using the field keyword
public int IntProp
{
  get;
  set => field = value * 2;
}

// set-only
public int SetOnlyProp { set => field = value; }

Whoa! So convenient!!
You won't need to declare the backing field manually anymore.

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/field

Are Properties Slow?

"Properties are functions under the hood, so they involve function calls. Therefore, they're slow. You should avoid them in performance-critical areas."

There is an occasional discussion like this.

Certainly, no matter how simple a C# property is, it translates to a function call when converted to intermediate code (IL). Looking at this, it seems like it might be slow due to the overhead of the call.

No Need to Worry About Auto-Implemented Properties

However!
If it's an auto-implemented property, you don't need to worry about it!

Optimizations occur at runtime, making them no different from fields.

public int Prop { get; set; }  // Auto-implemented property
public int Field;              // Field

public int GetProp() => Prop;  // Property access
public int GetField() => Field; // Field access
Resulting in the same JIT assembly code
C+Test.GetProp()
    L0000: mov eax, [ecx+4]
    L0003: ret

C+Test.GetField()
    L0000: mov eax, [ecx+8]
    L0003: ret

sharplab

Even in the old and performance-wise mediocre ".NET Framework" runtime, optimizations are applied so that there is almost no difference between auto-implemented properties and fields.

https://qiita.com/nabee_/items/cd12c3b53ca09ad3559f

Strictly speaking, there's a tiny difference, but if you're worrying about that, there are other things like algorithms that you should be focusing on more.

Cases to Watch Out For: Complex Properties and Asynchronous Processing

You need to be careful when you implement complex logic within a getter or setter. Since they look identical to fields from the outside, people might access them casually, which can lead to issues.

// Huh? I'm not heavy. I'm just a field (looking).
public int IamNotHeavy => DoHeavySomething();

// Oh no! (Heavy processing is executed every time!)
var p1 = this.IamNotHeavy;  // 1st time: Heavy processing runs
var p2 = this.IamNotHeavy;  // 2nd time: Heavy processing runs again

Also, properties cannot be async / await, so if you need to involve asynchronous processing, it's better to turn it into a method.

public int HeavyProp
{
  get {
    // You cannot await asynchronous processing inside a property
    // return await DoHeavySomethingAsync()

    // You could call it like this, but it will DEADLOCK!!!
    return DoHeavySomethingAsync().Result;
  }
}

Treat properties primarily as variables, and if that's not the case, stick to using methods.

Summary

Current State of Properties

  • Properties ≠ Simple Getters/Setters: In modern C#, their uses are broad and their importance is extremely high.
  • Auto-implemented Properties are Mainstream: Used in the form { get; set; }, allowing use without being conscious of getters/setters.
  • Official Recommendations: Use properties instead of fields for visible members (CA1051).

Benefits of Properties

  • Resilient to Specification Changes: You can change internal implementation without affecting the caller.
  • Improved Safety: Write robust code using init, required, and access control.
  • Performance: Auto-implemented properties perform almost identically to fields.

Where to Use Properties

  • MVVM Pattern: Play a central role in data binding within UI frameworks.
  • Source Generators: Act as hook points for automatic code generation via partial properties.
  • record Types: The underlying technology for immutable data types.

Points to Watch Out For

  • Avoid Heavy Processing: Hiding complex logic in properties can lead to unexpected delays.
  • No Asynchronous Processing: Since async/await cannot be used, turn it into a method if asynchronous processing is required.

Future of Properties

  • field Keyword (Planned for C# 14.0): Allows for more concise syntax by automatically generating backing fields.

Conclusion: In modern C#, properties are positioned as a core language feature that goes beyond simple getters/setters.

By moving a step forward from the traditional understanding of "property = getter/setter" and reframing them as "property = flexible and secure class members," you'll be able to write more effective C# code... hopefully!

脚注
  1. at the Recommended level or higher ↩︎

  2. The MVVM pattern itself reportedly originated with WPF ↩︎

  3. It's reportedly 20 times faster on NativeAOT than the traditional Regex syntax ↩︎

Discussion

junerjuner

ref 戻り値を使えば プロパティだって フィールドの様に 参照を返すことができるわかる(フィル―ドをプロパティにしてもそんな困ることはあんまないわかる)

using System;
{
    var v = new Class1();
    v.Value = 1;
    ref int value = ref v.Value;
    Console.WriteLine($"{v}");
    value = 2;
    Console.WriteLine($"{v}");
}
{
    var v = new Class2();
    v.Value = 1;
    ref int value = ref v.Value;
    Console.WriteLine($"{v}");
    value = 2;
    Console.WriteLine($"{v}");
}

public class Class1 {
    int _value;
    public ref int Value {get => ref _value;}
    public override string ToString() => $"{nameof(Class1)}{{{nameof(Value)}:{Value}}}";
}
public class Class2 {
    public int Value;
    public override string ToString() => $"{nameof(Class2)}{{{nameof(Value)}:{Value}}}";
}

https://sharplab.io/#v2:C4LgTgrgdgPgAgJgAwFgBQiCM6De6AEh+AbgIZgn4C8+UApgO74DCANqQM4eYAUAlAG4CRMHQBm+AJZRgJUqwh1q+UROIA6AGrzFQtEXxxMATh4ASAEQ5iAXwuDhhMgqU0Eeg0dOXrdh2htcRzkKYmV6JjZODgR+DxFxKRk5F2VVEi0dOnjCL3MrW3sclMVld2C8n0L/QLR0OABmQwQWdi5MfDx9ImlZAH1nXQqm9N78bVScAHM6WSoAPhVEgayBWs8mgHtiOjAwSQATJSMkfAAVTYBlYH2oKf5qRZ8oUgBbOk2xHij2vhscAEvd6fHgTRR/EA4MF0Gywix6WqNZqtaItLobJKyaHFJHbXb7I6GTCnC7XW73PiPfDPN4fL4/GJ/AE4IF00FZCFQrKwuwIoA=