Expression-Bodied Members · How it works
4 min readHow it works
Syntax
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;).
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.
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
=> 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.
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.
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 };.
A:
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
usingor atry/catch, ends before the task completes, so an elidedusing var db = …; return db.SaveAsync();disposes the context while the save is still running; - the method does not appear in async stack traces.
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
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.
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); }.
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 ….
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.