Expression-Bodied Members · How it works

4 min read
Mid-level7 min read
Rapid overview

How it works

Syntax

Q: What is an expression-bodied member?

A: A member whose body is a single expression after =>, instead of a statement block.

public class Person
{
    private string _name = "";

    public Person(string name) => Name = name;                        // constructor (C# 7)
    ~Person() => Console.WriteLine("finalized");                      // finalizer (C# 7)

    public string Name                                                // accessors (C# 7)
    {
        get => _name;
        set => _name = value ?? throw new ArgumentNullException(nameof(value));
    }

    public string Initials => $"{Name[0]}.";                          // read-only property (C# 6)
    public override string ToString() => $"Person {Name}";            // method (C# 6)
    public char this[int i] => Name[i];                               // indexer (C# 7)
    public static Person operator +(Person a, Person b)               // operator (C# 6)
        => new($"{a.Name}-{b.Name}");
}

Local functions and lambdas use the same form (int Twice(int x) => x * 2;).

Q: Which C# version added which members?

A: C# 6: methods, read-only properties, indexers with only a getter, and operators (including conversion operators). C# 7.0: constructors, finalizers, get/set accessors (and later init, C# 9), and full indexers. Events' add/remove accessors can also be expression-bodied.

Q: Can a void method be expression-bodied?

A: Yes, if the expression is a valid statement on its own: a method call, assignment, increment, await, or object creation. public void Log(string m) => _logger.LogInformation(m); is fine; public void X() => 42; is not.


What it compiles to

Q: Is there any performance difference between => and a block body?

A: No. public int Double(int x) => x 2; and public int Double(int x) { return x 2; } produce identical IL. The choice is purely about readability.

Q: What does an expression-bodied property compile to, and does it store anything?

A: Only a get_X method; no backing field. The expression is evaluated every time the property is read. Compare the three forms:

public DateTime A { get; } = DateTime.Now;  // auto-property + initializer: stored once, at construction
public DateTime B => DateTime.Now;          // expression-bodied: recomputed on every read
public DateTime C = DateTime.Now;           // public FIELD with initializer (no property at all)

A returns the same time forever, B a new time on each read, and C is a field (no binary-compatible contract, not serialized by default). One character (= vs =>) changes the semantics, which makes this a favourite interview question.

Q: Does it matter that an expression-bodied property is recomputed on every read?

A: Only when the expression is expensive or allocates. public decimal Total => _lines.Sum(l => l.Amount); walks the list every time; in a loop that reads Total repeatedly, cache it in a local, or store and update the total when the lines change. public IReadOnlyList<Line> Lines => _lines.AsReadOnly(); allocates a wrapper on every read; returning _lines typed as IReadOnlyList<Line> does not.


Useful patterns

  • Computed properties: public string FullName => $"{First} {Last}";, public bool IsOverdue => DueDate < DateTime.UtcNow;.
  • Forwarding and wrappers: public int Count => _items.Count;, public Task<User?> GetAsync(int id, CancellationToken ct) => _inner.GetAsync(id, ct);.
  • Guard-and-assign accessors with throw expressions: set => _email = value ?? throw new ArgumentNullException(nameof(value));.
  • Short constructors using tuple deconstruction: public Point(int x, int y) => (X, Y) = (x, y);.
  • ToString, Equals, operators: public static Money operator +(Money a, Money b) => a.Add(b);.
  • Switch expressions: public decimal Rate => Tier switch { Tier.Gold => 0.2m, Tier.Silver => 0.1m, _ => 0m };.
Q: What are good uses of expression-bodied members?

A:

Q: When should you not use one?

A: When the body has several steps, side effects that deserve their own lines, error handling, or logging; when you would have to cram logic into nested ternaries to fit one expression; and when the line becomes hard to read. The team style rule (.editorconfig csharp_style_expression_bodied_methods etc.) usually says "when on a single line".


Async and forwarding: the subtle trap

  • an exception thrown synchronously before the inner method returns its task propagates immediately from SaveAsync() instead of being stored in the returned task;
  • anything wrapped around the call, such as a using or a try/catch, ends before the task completes, so an elided using var db = …; return db.SaveAsync(); disposes the context while the save is still running;
  • the method does not appear in async stack traces.
Q: Is public Task SaveAsync() => _repo.SaveAsync(); the same as public async Task SaveAsync() => await _repo.SaveAsync();?

A: The result is usually the same, but the behaviour differs in edge cases. The version without async ("eliding" async/await) returns the inner task directly: it is slightly cheaper (no state machine), but:

Eliding is fine for pure one-line forwarding with nothing around it. As soon as there is a using, try or more than one call, use async/await.


Common interview gotchas

Q: What is the difference between public int X => 5; and public int X = 5;?

A: The first is a read-only property that returns 5 (a method with no storage); the second is a public mutable field initialized to 5.

Q: Can an expression-bodied property have a setter?

A: The shorthand public int X => …; is get-only. For a setter, write the accessors separately, each with its own expression body: public int X { get => _x; set => _x = Math.Max(0, value); }.

Q: Can you use throw in an expression-bodied member?

A: Yes. Since C# 7.0, throw is an expression in these positions: public void NotSupported() => throw new NotSupportedException(); and => value ?? throw ….

Q: What does public List<int> Items => new(); do when a caller adds to it?

A: The caller adds to a brand-new list that is immediately discarded, because every read creates another list. Use { get; } = new(); for a stored list.