question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Mark some properties and methods as `readonly`

See original GitHub issue

Background and motivation

Some properties and methods should be marked readonly. This way they can be used on readonly references without causing defensive copies.

This is something I ran into recently with Padding, so I went and found (hopefully) all of the types which could be improved by marking them / some of their members as readonly. I have an analyser which detected a defensive copy for my use of Padding, and I have currently worked around it by using Unsafe.AsRef (obviously I could have also accepted the defensive copy) - but I would like to remove this workaround as it should be unnecessary.

Note that Point, PointF, etc. have already had this done to them in https://github.com/dotnet/corefx/pull/40407.

I would like to make a PR for this when approved.

API Proposal

namespace System.Drawing
{
	//`: IEquatable<CharacterRange>` is currently not in ref-only dll, but is in source dll:
-	public struct CharacterRange
+	public struct CharacterRange : IEquatable<CharacterRange>
	{
-		public int First { get; set; }
+		public int First { readonly get; set; }
-		public int Length { get; set; }
+		public int Length { readonly get; set; }
-		public override bool Equals(object? obj);
+		public override readonly bool Equals(object? obj);
		//`bool Equals(CharacterRange other)` is currently not in ref-only dll, but is in source dll:
+		public readonly bool Equals(CharacterRange other);
-		public override int GetHashCode();
+		public override readonly int GetHashCode();
		//...
	}
}

namespace System.Windows.Forms
{
-	public struct BindingMemberInfo
+	public readonly struct BindingMemberInfo
	{
		public string BindingPath { get; }
		public string BindingField { get; }
		public string BindingMember { get; }
		public override bool Equals(object? otherObject);
		public bool Equals(BindingMemberInfo other);
		public override int GetHashCode();
		//...
	}
	//or we could make each member readonly instead? (applicable members listed above if this is what we do)

-	public struct ImeModeConversion
+	public readonly struct ImeModeConversion
	{
		//...
	}

	public struct LinkArea
	{
-		public int Start { get; set; }
+		public int Start { readonly get; set; }
-		public int Length { get; set; }
+		public int Length { readonly get; set; }
-		public bool IsEmpty { get; }
+		public bool IsEmpty { readonly get; }
-		public override bool Equals(object? o);
+		public override readonly bool Equals(object? o);
-		public bool Equals(LinkArea other);
+		public readonly bool Equals(LinkArea other);
-		public override string ToString();
+		public override readonly string ToString();
-		public override int GetHashCode();
+		public override readonly int GetHashCode();
		//...
	}

	public struct Message
	{
-		public int Msg { get; set; }
+		public int Msg { readonly get; set; }
-		public IntPtr WParam { get; set; }
+		public IntPtr WParam { readonly get; set; }
-		public IntPtr LParam { get; set; }
+		public IntPtr LParam { readonly get; set; }
-		public IntPtr Result { get; set; }
+		public IntPtr Result { readonly get; set; }
-		public object? GetLParam(Type cls);
+		public readonly object? GetLParam(Type cls);
-		public override bool Equals(object? o);
+		public override readonly bool Equals(object? o);
-		public bool Equals(Message other);
+		public readonly bool Equals(Message other);
-		public override int GetHashCode();
+		public override readonly int GetHashCode();
-		public override string ToString();
+		public override readonly string ToString();
		//...
	}

	public struct Padding
	{
-		public int All { get; set; }
+		public int All { readonly get; set; }
-		public int Bottom { get; set; }
+		public int Bottom { readonly get; set; }
-		public int Left { get; set; }
+		public int Left { readonly get; set; }
-		public int Right { get; set; }
+		public int Right { readonly get; set; }
-		public int Top { get; set; }
+		public int Top { readonly get; set; }
-		public int Horizontal { get; }
+		public int Horizontal { readonly get; }
-		public int Vertical { get; }
+		public int Vertical { readonly get; }
-		public Size Size { get; }
+		public Size Size { readonly get; }
-		public override bool Equals(object? other);
+		public override readonly bool Equals(object? other);
-		public bool Equals(Padding other);
+		public readonly bool Equals(Padding other);
-		public override int GetHashCode();
+		public override readonly int GetHashCode();
-		public override string ToString();
+		public override readonly string ToString();
		//...
	}

	public struct TableLayoutPanelCellPosition
	{
-		public override bool Equals(object? other);
+		public override readonly bool Equals(object? other);
-		public bool Equals(TableLayoutPanelCellPosition other);
+		public readonly bool Equals(TableLayoutPanelCellPosition other);
-		public override string ToString();
+		public override readonly string ToString();
-		public override int GetHashCode();
+		public override readonly int GetHashCode();
		//...
	}
}

Pre-existing attributes omitted for clarity.

Edit: Removed TableLayoutPanelCellPosition.Row, TableLayoutPanelCellPosition.Column, Message.HWnd, and all of the properties on TextMetrics, as these were already marked as readonly due to being auto properties.

Edit 2: Add implementation of IEquatable<CharacterRange> to CharacterRange + its method, since it’s declared in source, but not in the ref project.

Edit 3: Add missing Padding.GetHashCode() and Padding.ToString() members.

Edit 4 (question): Should we fix the NotSupported.cs’s definition of CharacterRange to have the correct fields. It doesn’t really matter, but would better match what C# would generate for a ref assembly.

API Usage

The applicable APIs can now be used anywhere that these types are declared as readonly without causing a defensive copy, thus improving performance.

e.g. (a simplified version of my scenario)

struct SomeLargeStruct
{
	public Padding Padding;
	//...
}

int SomeMethod(in SomeLargeStruct referenceToABunchOfFields, int index)
{
	//some condition
	if (condition) return referenceToABunchOfFields.Padding.Top;
}

Alternative Designs

For BindingMemberInfo, we could do:

namespace System.Windows.Forms
{
	public struct BindingMemberInfo
	{
-		public string BindingPath { get; }
+		public readonly string BindingPath { get; }
-		public string BindingField { get; }
+		public readonly string BindingField { get; }
-		public string BindingMember { get; }
+		public readonly string BindingMember { get; }
-		public override bool Equals(object? otherObject);
+		public override readonly bool Equals(object? otherObject);
-		public bool Equals(BindingMemberInfo other);
+		public bool readonly Equals(BindingMemberInfo other);
-		public override int GetHashCode();
+		public override readonly int GetHashCode();
	}
}

instead of

namespace System.Windows.Forms
{
-	public struct BindingMemberInfo
+	public readonly struct BindingMemberInfo
	{
		public string BindingPath { get; }
		public string BindingField { get; }
		public string BindingMember { get; }
		public override bool Equals(object? otherObject);
		public bool Equals(BindingMemberInfo other);
		public override int GetHashCode();
	}
}

Risks

This should be pretty uncontroversial.

Maybe I have missed an API somewhere, I’m pretty sure I got all the public structs, but I may have missed a nested one or something like that.

Will this feature affect UI controls?

I would hope not.

Issue Analytics

  • State:closed
  • Created 2 months ago
  • Reactions:2
  • Comments:10 (10 by maintainers)

github_iconTop GitHub Comments

2reactions
JeremyKuhnecommented, Aug 3, 2023

We don’t have to do the formal API review. I’m going to try and find some time tomorrow to sanity check this. We can never remove readonly so I want to take another look to make sure I’ve thought it through a bit for each of these.

1reaction
elachlancommented, Aug 2, 2023

@hamarb123 I’ll leave the PR to you if you have time. Let me know if you want me to jump in and get it done.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Assign value to `readonly` properties from methods called ...
I have a simple class, and I want to assign a value to a readonly property in a method initiated by the constructor,...
Read more >
Read-Only Properties in TypeScript
Properties marked with readonly can only be assigned to during initialization or from within a constructor of the same class. All other ...
Read more >
TypeScript - ReadOnly
TypeScript introduced readonly keyword which makes a property as read-only in the class, type or interface.
Read more >
Readonly Properties in TypeScript
In this post you'll learn how and when to use the readonly keyword for in TypeScript. The readonly property can be used in...
Read more >
ReadOnlyAttribute Class (System.ComponentModel)
The PropertyDescriptor class enforces the ReadOnlyAttribute in the design environment and at run time. When you mark a property with the ReadOnlyAttribute set...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found