RefactorMCP catalog

Extract Local Variable

Also known as Introduce Variable. Declares a local holding a selected expression just before the statement that contains it, and uses the local in place of the expression. The reverse of Inline Local Variable.

Target

The expression, by a selection. The argument name is the local's name.

Precondition

  • The selection is an expression inside a statement of a block-bodied member.
  • The expression has a value.
  • The expression is evaluated every time the statement runs, and only then: it is not in a loop condition or increment, the right of &&, || or ??, a branch of ?:, the part after ?., a switch expression arm, or a lambda.
  • It reads no variable declared inside the statement outside the selection, such as the parameter of an enclosing lambda.
  • name is not a local or parameter visible at the statement, and does not appear in the rest of the block, where the local would clash with or hide it.

Transformation

  • The local is declared with the expression's type, written as briefly as the scope allows, with its nullable annotation. An anonymous type is declared with var.
  • The declaration goes directly before the statement. A statement that is the body of an if, loop or similar without braces is wrapped in a block to hold it.
  • The selected expression is replaced by the local, dropping parentheses that only grouped it. When the expression has no side effects, identical expressions elsewhere in the same statement that are evaluated whenever it is are replaced as well. Other statements are not touched.

Preserved

  • Behaviour: the expression is evaluated once, at the same point in the statement's evaluation or earlier, and only where nothing it reads can have changed.
  • Comments above the statement stay above the new declaration; a trailing comment stays on the statement.

Limitations

  • Moving the evaluation of an expression with side effects to before the statement runs it ahead of the parts of the statement that preceded it.
  • Occurrences in other statements are not replaced, even when they would see the same value.
  • Expression-bodied members are refused rather than converted to a block body.

Error codes

CodeMeaning
expression-bodied-memberthe expression is in an expression-bodied member
not-in-statementthe expression is not inside a statement, such as a field initializer
in-loop-conditionthe expression is in a loop condition or increment
conditionally-evaluatedthe expression runs only on some paths, or later
declared-in-statementthe expression reads a variable declared inside the statement
name-conflictname is already declared or used where the local would be
void-expressionthe expression has no value
not-an-expressionthe selection is not an expression

Cases

·

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

#conditionally-evaluated-rejected

Refuses an expression evaluated only on some paths, which would then run on every path

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"extracted"
Refusesconditionally-evaluated; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public bool HasItems(string text)
4 {
5 return text != null && /*[*/text.Length/*]*/ > 0;
6 }
7 }

#declared-in-statement-rejected

Refuses an expression that reads a lambda parameter, which does not exist before the statement

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"extracted"
Refusesdeclared-in-statement; every file is left unchanged
Sample.csinput
1 using System.Linq;
2
3 public class Sample
4 {
5 public int[] Doubled(int[] values)
6 {
7 return values.Select(v => /*[*/v * 2/*]*/).ToArray();
8 }
9 }

#embedded-statement

An expression in a statement without a block gets a block to hold the declaration

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"tax"
Sample.csmodified
11 public class Sample
22 {
33 public decimal Tax(bool taxable, decimal total, decimal rate)
44 {
55 if (taxable)
6− return /*[*/total * rate/*]*/;
6+ {
7+ decimal tax = total * rate;
8+ return tax;
9+ }
710 return 0;
811 }
912 }

#expression-bodied-rejected

Refuses an expression in an expression-bodied member, which has no statement to put a declaration before

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"extracted"
Refusesexpression-bodied-member; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Calc(int a, int b) => /*[*/a * b/*]*/ + 1;
4 }

#generic-type

The local's type is written with the method's type parameters

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"items"
Sample.csmodified
11 using System.Collections.Generic;
22
33 public class Sample
44 {
55 public int Count<T>(T item)
66 {
7− return Measure(/*[*/new List<T> { item, item }/*]*/);
7+ List<T> items = new List<T> { item, item };
8+ return Measure(items);
89 }
910
1011 private int Measure<T>(List<T> items) => items.Count;
1112 }

#keeps-comments

A comment above the statement stays above the new declaration, and a trailing comment stays on the statement

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"discount"
Sample.csmodified
11 public class Sample
22 {
33 public decimal Net(decimal price, decimal rate)
44 {
55 var total = price;
66
77 // Apply the discount.
8− return total - /*[*/price * rate/*]*/; // before tax
8+ decimal discount = price * rate;
9+ return total - discount; // before tax
910 }
1011 }

#loop-condition-rejected

Refuses an expression in a loop condition, which is evaluated again on every iteration

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"extracted"
Refusesin-loop-condition; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Last(int[] items)
4 {
5 var i = 0;
6 while (i < /*[*/items.Length - 1/*]*/)
7 {
8 i++;
9 }
10
11 return i;
12 }
13 }

#name-conflict-rejected

Refuses a name already used by a parameter in scope

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"a"
Refusesname-conflict; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Sum(int a, int b)
4 {
5 return /*[*/a + b/*]*/;
6 }
7 }

#not-an-expression-rejected

Refuses a selection that is not an expression, such as a method's name

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"extracted"
Refusesnot-an-expression; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int /*[*/Calc/*]*/(int a, int b)
4 {
5 return a + b;
6 }
7 }

#not-in-statement-rejected

Refuses an expression in a field initializer

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"extracted"
Refusesnot-in-statement; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 private int _limit = /*[*/10 * 2/*]*/;
4
5 public int Limit() => _limit;
6 }

#nullable-context

The local's type carries the expression's nullable annotation

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"trimmed"
Projectnullable enable
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(int id)
44 {
5− return /*[*/Find(id)?.Trim()/*]*/ ?? "none";
5+ string? trimmed = Find(id)?.Trim();
6+ return trimmed ?? "none";
67 }
78
89 private string? Find(int id) => id > 0 ? " found " : null;
910 }

#occurrences-in-statement

Identical side-effect free expressions in the same statement use the local too; other members are untouched

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"sum"
Sample.csmodified
11 public class Sample
22 {
33 public int Square(int a, int b)
44 {
5− return (/*[*/a + b/*]*/) * (a + b);
5+ int sum = a + b;
6+ return sum * sum;
67 }
78
89 public int Sum(int a, int b)
910 {
1011 return a + b;
1112 }
1213 }

#side-effects-selection-only

An expression with side effects replaces only the selected occurrence, so it still runs as often as before

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"first"
Sample.csmodified
11 public class Sample
22 {
33 private int _counter;
44
55 public int Pair()
66 {
7− return /*[*/Next()/*]*/ * 10 + Next();
7+ int first = Next();
8+ return first * 10 + Next();
89 }
910
1011 private int Next() => ++_counter;
1112 }

#simple-expression

Declares a local of the expression's type just before the statement and uses it in place of the expression

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"factor"
Sample.csmodified
11 public class Sample
22 {
33 public decimal Discounted(decimal price, decimal rate)
44 {
5− return price * (/*[*/1 - rate/*]*/);
5+ decimal factor = 1 - rate;
6+ return price * factor;
67 }
78 }

#void-expression-rejected

Refuses an expression with no value

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"extracted"
Refusesvoid-expression; every file is left unchanged
Sample.csinput
1 using System;
2
3 public class Sample
4 {
5 public void Log(string text)
6 {
7 /*[*/Console.WriteLine(text)/*]*/;
8 }
9 }