RefactorMCP catalog

Convert Local to Field

Promotes a local variable to a private field of the containing type. The declaration becomes an assignment to the field and every use refers to the field. A step of Replace Method with Method Object.

Target

The local, by a caret on its declaration or on any use of it. In a later step of a composite, the containing member's symbol with arguments.local naming the local. The optional argument name is the field's name; it defaults to the local's.

Precondition

  • The local is declared by a local declaration statement in a block or switch section that declares only it, and is not a using declaration or a ref local.
  • Its type can be written as a field's: it is not anonymous and does not use a type parameter of the method.
  • The type and its bases have no member with the field's name, and no other local or parameter of the member has it, which would hide the field.

Transformation

  • A private field of the local's type is added after the type's last field, or as its first member, followed by a blank line, when it has none. The field is static when the member declaring the local is static.
  • In a nullable context a reference type field is nullable, since it holds nothing until the member first assigns it.
  • A declaration with an initializer becomes an assignment to the field at the same place. A declaration without one is removed, and the comments above it stay in place.
  • A const local becomes a private const field with its value, and its declaration is removed.
  • Every use of the local refers to the field.

Preserved

  • Behaviour of a single, non-reentrant call of the member.
  • Comments above the declaration.

Limitations

  • The field outlives the call, so recursion, re-entrancy and concurrent calls now share one value. This is the point of the refactoring, but it is not checked.
  • Locals declared by for, using, foreach or pattern statements are not covered.

Error codes

CodeMeaning
name-conflictthe type or a base already has a member with the field's name
hidden-by-localanother local or parameter of the member has the field's name
uses-method-type-parameterthe local's type uses a type parameter of the method
using-declarationthe local is a using declaration, disposed when the member ends
multiple-declaratorsthe statement declares several locals
anonymous-typethe local's type is anonymous and cannot be written
not-a-localthe target is not a local variable

Cases

·

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

#anonymous-type-rejected

Refuses a local of anonymous type, which a field cannot be declared with

refusal
TargetSample.cs, the /*^*/ caret
Arguments
name"_point"
Refusesanonymous-type; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public string Describe()
4 {
5 var /*^*/point = new { X = 1, Y = 2 };
6 return point.X + "," + point.Y;
7 }
8 }

#by-symbol

In a later step of a composite the local is named by argument within the method's symbol

success
Targetsymbol M:Shop.Order.Total(System.Int32)
Arguments
local"sum"
name"_sum"
Order.csmodified
11 namespace Shop
22 {
33 public class Order
44 {
55 private int _price = 5;
6+ private int _sum;
67
78 public int Total(int quantity)
89 {
9− var sum = _price * quantity;
10− return sum;
10+ _sum = _price * quantity;
11+ return _sum;
1112 }
1213 }
1314 }

#const-local

A constant local becomes a private constant field and its declaration is removed

success
TargetSample.cs, the /*^*/ caret
Arguments
name"Limit"
Sample.csmodified
11 public class Sample
22 {
33 private int _count;
4+ private const int Limit = 10;
45
56 public bool IsFull()
67 {
7− const int /*^*/limit = 10;
8− return _count >= limit;
8+ return _count >= Limit;
99 }
1010 }

#generic-class

A local whose type uses the class's type parameter becomes a field of that type

success
TargetSample.cs, the /*^*/ caret
Arguments
name"_items"
Sample.csmodified
11 using System.Collections.Generic;
22
33 public class Cache<T>
44 {
5+ private List<T> _items;
6+
57 public int Store(T item)
68 {
7− var /*^*/items = new List<T>();
8− items.Add(item);
9− return items.Count;
9+ _items = new List<T>();
10+ _items.Add(item);
11+ return _items.Count;
1012 }
1113 }

#hidden-by-local-rejected

Refuses a field name that a parameter of the method already has, which would hide the field

refusal
TargetSample.cs, the /*^*/ caret
Arguments
name"count"
Refuseshidden-by-local; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Add(int count)
4 {
5 var /*^*/sum = count + 1;
6 return sum;
7 }
8 }

#instance-method

Promotes a local to a private field; the declaration becomes an assignment and every use refers to the field

success
TargetSample.cs, the /*^*/ caret
Arguments
name"_total"
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
5+ private int _total;
6+
57 public void Add(int a, int b)
68 {
7− var /*^*/total = a + b;
8− Console.WriteLine(total);
9+ _total = a + b;
10+ Console.WriteLine(_total);
911 }
1012 }

#keeps-name-and-comments

Without a new name the field takes the local's; a declaration with no value is removed with its comment kept

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
5+ private string mode;
6+
57 public void Run(bool fast)
68 {
79 // Chosen below.
8− string /*^*/mode;
910 if (fast)
1011 {
1112 mode = "fast";
1213 }
1314 else
1415 {
1516 mode = "slow";
1617 }
1718
1819 Console.WriteLine(mode);
1920 }
2021 }

#method-type-parameter-rejected

Refuses a local whose type uses a type parameter of the method, which a field cannot name

refusal
TargetSample.cs, the /*^*/ caret
Arguments
name"_first"
Refusesuses-method-type-parameter; every file is left unchanged
Sample.csinput
1 using System.Collections.Generic;
2
3 public class Sample
4 {
5 public T First<T>(List<T> items)
6 {
7 T /*^*/first = items[0];
8 return first;
9 }
10 }

#multiple-declarators-rejected

Refuses a declaration of several locals

refusal
TargetSample.cs, the /*^*/ caret
Arguments
name"_a"
Refusesmultiple-declarators; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Sum()
4 {
5 int /*^*/a = 1, b = 2;
6 return a + b;
7 }
8 }

#name-conflict-rejected

Refuses a field name the type already has a member of

refusal
TargetSample.cs, the /*^*/ caret
Arguments
name"_count"
Refusesname-conflict; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 private int _count;
4
5 public int Add(int a)
6 {
7 var /*^*/sum = _count + a;
8 return sum;
9 }
10 }

#nullable-context

In a nullable context the field is nullable, since it holds no value until the method runs

success
TargetSample.cs, the /*^*/ caret
Arguments
name"_name"
Projectnullable enable
Sample.csmodified
11 public class Sample
22 {
3+ private string? _name;
4+
35 public int Length(string text)
46 {
5− string /*^*/name = text.Trim();
6− return name.Length;
7+ _name = text.Trim();
8+ return _name.Length;
79 }
810 }

#parameter-rejected

Refuses a parameter, which is not a local

refusal
TargetSample.cs, the /*^*/ caret
Arguments
name"_value"
Refusesnot-a-local; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Twice(int /*^*/value)
4 {
5 return value * 2;
6 }
7 }

#static-method

A local of a static method becomes a static field, placed after the existing fields

success
TargetSample.cs, the /*^*/ caret
Arguments
name"_last"
Sample.csmodified
11 public static class Sample
22 {
33 private static int _calls;
4+ private static int _last;
45
56 public static int Square(int value)
67 {
78 _calls++;
8− int /*^*/result = value * value;
9− return result;
9+ _last = value * value;
10+ return _last;
1011 }
1112 }

#using-declaration-rejected

Refuses a using declaration, whose disposal ends with the method

refusal
TargetSample.cs, the /*^*/ caret
Arguments
name"_reader"
Refusesusing-declaration; every file is left unchanged
Sample.csinput
1 using System.IO;
2
3 public class Sample
4 {
5 public int Length(string text)
6 {
7 using var /*^*/reader = new StringReader(text);
8 return reader.ReadToEnd().Length;
9 }
10 }