RefactorMCP catalog

Make Field Readonly

Adds readonly to a field that is only assigned while an object is being constructed, so the compiler enforces what the code already does.

Precondition

  • The field is not const or volatile.
  • Every assignment, increment and ref or out use of the field, in any file, is in a constructor or init accessor of the declaring type itself, through this rather than another instance. A static field may only be assigned in the static constructor. An assignment in a lambda or local function counts as outside the constructor, since it may run later.
  • The field's type is not a mutable struct declared in the solution: calls through a readonly field would act on a defensive copy, so a method such as Add() would silently stop changing the field.
  • The field is declared on its own, since readonly on a shared declaration would apply to every field in it.

Transformation

  • readonly is added after the access and static modifiers.
  • The initialiser stays on the field. A readonly field may keep one, and moving it into constructors would change when it runs relative to the constructor body.
  • A field that is already readonly is left unchanged.

Preserved

  • Everything; the change only adds a constraint the code already meets.
  • Comments on the field.

Limitations

  • Structs from referenced assemblies are not inspected, so a mutable struct from a library is not refused.

Error codes

CodeMeaning
constant-fieldthe field is const
volatile-fieldthe field is volatile
mutable-structthe field holds a mutable struct declared in the solution
multiple-declaratorsthe field is declared alongside others
assigned-outside-constructorthe field is assigned somewhere other than construction

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#already-readonly

A field that is already readonly is left as it is

success
Targetsymbol F:Shop.Sample._limit
Sample.csunchanged
11 namespace Shop
22 {
33 public class Sample
44 {
55 private readonly int _limit = 5;
66
77 public int Limit() => _limit;
88 }
99 }

#assigned-in-constructor

A field only assigned by the constructor becomes readonly

success
Targetsymbol F:Shop.Customer._name
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
5− private string _name;
5+ private readonly string _name;
66
77 public Customer(string name)
88 {
99 _name = name;
1010 }
1111
1212 public string Greeting() => "Dear " + _name;
1313 }
1414 }

#assigned-in-constructor-lambda-rejected

Refuses a field assigned by a lambda written in the constructor, which may run after construction

refusal
Targetsymbol F:Shop.Button._clicks
Refusesassigned-outside-constructor; every file is left unchanged
Button.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Button
6 {
7 private int _clicks;
8 private readonly Action _onClick;
9
10 public Button()
11 {
12 _onClick = () => _clicks = _clicks + 1;
13 }
14
15 public void Click() => _onClick();
16
17 public int Clicks() => _clicks;
18 }
19 }

#assigned-outside-constructor-rejected

Refuses a field that a method assigns

refusal
Targetsymbol F:Shop.Counter._count
Refusesassigned-outside-constructor; every file is left unchanged
Counter.csinput
1 namespace Shop
2 {
3 public class Counter
4 {
5 private int _count;
6
7 public void Increment() => _count++;
8
9 public int Count() => _count;
10 }
11 }

#constant-rejected

Refuses a constant

refusal
Targetsymbol F:Shop.Sample.Limit
Refusesconstant-field; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public const int Limit = 5;
6 }
7 }

#initializer-stays

An initialiser stays where it is, even when the type has a constructor, and comments on the field are kept

success
Targetsymbol F:Shop.Config._timeout
Config.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Config
66 {
77 // Seconds before a request is abandoned.
8− private int _timeout = 30;
8+ private readonly int _timeout = 30;
99
1010 public Config()
1111 {
1212 Console.WriteLine(_timeout);
1313 }
1414
1515 public int Timeout() => _timeout;
1616 }
1717 }

#mutable-struct-rejected

Refuses a field holding a mutable struct, whose methods would then change a copy

refusal
Targetsymbol F:Shop.Meter._tally
Refusesmutable-struct; every file is left unchanged
Meter.csinput
1 namespace Shop
2 {
3 public struct Tally
4 {
5 public int Value;
6
7 public void Add() => Value++;
8 }
9
10 public class Meter
11 {
12 private Tally _tally;
13
14 public void Tick() => _tally.Add();
15
16 public int Reading() => _tally.Value;
17 }
18 }

#nullable-and-init-accessor

A nullable field assigned by an init accessor becomes readonly, which init accessors may assign

success
Targetsymbol F:Shop.Profile._nickname
Projectnullable enable
Profile.csmodified
11 namespace Shop
22 {
33 public class Profile
44 {
5− private string? _nickname;
5+ private readonly string? _nickname;
66
77 public string? Nickname
88 {
99 get => _nickname;
1010 init => _nickname = value;
1111 }
1212 }
1313 }

#partial-constructor-in-another-file

A constructor in another part of a partial type may assign the field

success
Targetsymbol F:Shop.Order._id
Order.csmodified
11 namespace Shop
22 {
33 public partial class Order
44 {
5− private int _id;
5+ private readonly int _id;
66
77 public int Id() => _id;
88 }
99 }
Order.Create.csunchanged
11 namespace Shop
22 {
33 public partial class Order
44 {
55 public Order(int id)
66 {
77 _id = id;
88 }
99 }
1010 }

#static-field-in-static-constructor

A static field assigned only by the static constructor becomes static readonly

success
Targetsymbol F:Shop.Registry._started
Registry.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public static class Registry
66 {
7− private static DateTime _started;
7+ private static readonly DateTime _started;
88
99 static Registry()
1010 {
1111 _started = DateTime.UtcNow;
1212 }
1313
1414 public static TimeSpan Uptime() => DateTime.UtcNow - _started;
1515 }
1616 }