December Code

C# LINQ and lambda interview questions, with answers

LINQ is the part of C# interviewers use to separate people who write C# from people who read it: a query that looks like a single line runs at a moment you have to predict, may run twice, or may be translated into SQL. The questions also cover the machinery underneath, delegates, events and lambdas, and the closure rules that decide what a lambda sees when it finally runs. Every answer below comes with code whose output was produced by compiling and running it on .NET 10.

The questions start with the two ways of writing a query, move through when and how queries run, and end with the delegates and closures they are made of. Then take the free C# diagnostic — ten questions across every C# topic in the bank — to see which of these you can explain but not yet predict.

The questions, with answers

  1. 1.What is LINQ, and what is the difference between query syntax and method syntax?

    In short: LINQ is a set of query operators over any sequence; query syntax, from x in xs where ... select ..., compiles into the same Where and Select calls that method syntax writes directly.

    Language Integrated Query lets you filter, project, sort, group and join any sequence with the same operators, whether it holds objects in memory, XML nodes or rows in a database. Query syntax reads like SQL, and the compiler translates it mechanically into method calls, so from x in xs where x > 2 orderby x select x * 10 becomes xs.Where(x => x > 2).OrderBy(x => x).Select(x => x * 10), and the two produce the same results, as below. Method syntax covers every operator, including Count, Distinct, Take and Zip, which have no query keywords, while query syntax reads better for joins, several from clauses and let. Most code bases use method syntax and switch to query syntax for complex joins.

    int[] xs = { 5, 1, 4, 3 };
    var q1 = from x in xs
        where x > 2
        orderby x
        select x * 10;
    var q2 = xs.Where(x => x > 2)
        .OrderBy(x => x)
        .Select(x => x * 10);
    Console.WriteLine(
        string.Join(",", q1));
    Console.WriteLine(
        q1.SequenceEqual(q2));
    // 30,40,50
    // True
  2. 2.What is deferred execution in LINQ, and when does a query actually run?

    In short: Most operators only build a query; the work happens each time the result is enumerated, so later changes to the source are seen, and enumerating twice runs the query twice.

    Where, Select, OrderBy and most other operators return an object that remembers the source and the steps, and nothing is computed until something enumerates it, with foreach, ToList, Count or First. That makes queries composable and lazy, but interviewers probe two consequences. A query sees the source as it is when enumerated, not as it was when the query was written. And each enumeration reruns the whole query, so a query with a side effect or an expensive source, such as a database call, runs again every time, as the counter below shows: Sum and Max each run the projection three times. ToList() or ToArray() executes the query once and keeps the results, which is the usual fix.

    int calls = 0;
    var q = new[] { 1, 2, 3 }
        .Select(x =>
        {
            calls++;
            return x * 2;
        });
    var a = q.Sum();
    var b = q.Max();
    Console.WriteLine(calls);
    var cached = q.ToList();
    Console.WriteLine(calls);
    // 6
    // 9
  3. 3.What is the difference between First, FirstOrDefault, Single and SingleOrDefault?

    In short: First returns the first match and throws if there is none; Single demands exactly one match; the OrDefault versions return a default instead of throwing when nothing matches.

    First stops at the first element that matches and throws InvalidOperationException when the sequence has none. FirstOrDefault returns default(T) instead, which is null for reference types and 0 for int, awkward when 0 is also a valid value; since .NET 6 an overload takes the fallback to use. Single checks the whole sequence and throws if there is no match or more than one, which makes it an assertion that a key is unique, as the duplicate below shows. SingleOrDefault returns the default when nothing matches but still throws for several matches. Use First when order matters and you want the first, Single when more than one match would mean corrupt data, and the OrDefault forms when absence is normal.

    string[] ids =
        { "a1", "b2", "a3" };
    Console.WriteLine(ids.First(
        s => s.StartsWith('a')));
    string? z = ids.FirstOrDefault(
        s => s.StartsWith('z'));
    Console.WriteLine(z ?? "none");
    try
    {
        ids.Single(s =>
            s.StartsWith('a'));
    }
    catch (Exception e)
    {
        Console.WriteLine(
            e.GetType().Name);
    }
    // a1
    // none
    // InvalidOperationException
  4. 4.What is the difference between Select and SelectMany in LINQ?

    In short: Select maps each element to one result, so a sequence of lists stays nested; SelectMany maps each element to a sequence and flattens them all into one.

    Select(x => f(x)) produces exactly one output for each input. When f returns a collection, the result is nested: selecting each team's members gives a sequence of arrays. SelectMany(x => f(x)) enumerates each returned collection and concatenates them, so the same call gives every member of every team in a single flat sequence, as below. It is the method behind a query with two from clauses, and an overload takes a result selector so that each output can combine the parent and the child, such as a team name paired with each member. In other languages the same operation is called flatMap.

    var teams = new[]
    {
        new[] { "Ann", "Raj" },
        new[] { "Li" }
    };
    var sizes = teams
        .Select(t => t.Length);
    var all = teams
        .SelectMany(t => t);
    Console.WriteLine(
        string.Join(",", sizes));
    Console.WriteLine(
        string.Join(",", all));
    // 2,1
    // Ann,Raj,Li
  5. 5.What is the difference between IEnumerable<T> and IQueryable<T> in LINQ?

    In short: LINQ on IEnumerable<T> runs compiled delegates in memory; LINQ on IQueryable<T> builds an expression tree that a provider, such as Entity Framework, translates and runs elsewhere, typically as SQL.

    Each operator exists twice. Enumerable.Where takes a Func<T, bool>, compiled code that runs on each element in your process. Queryable.Where takes an Expression<Func<T, bool>>, a data structure describing the lambda, which a provider can inspect and translate, so a query over a database table becomes a SQL WHERE clause and only matching rows travel over the network. The same lambda text can become either, as below, where the expression can be printed and compiled. The static type of the variable decides which is used: calling AsEnumerable() or ToList() on a query switches the rest of it to in-memory execution, a classic cause of pulling a whole table into memory.

    using System.Linq.Expressions;
    
    Func<int, bool> f = x => x > 3;
    Expression<Func<int, bool>> e =
        x => x > 3;
    Console.WriteLine(f(5));
    Console.WriteLine(e);
    var g = e.Compile();
    Console.WriteLine(g(2));
    // True
    // x => (x > 3)
    // False
  6. 6.What is the difference between a delegate and an event in C#?

    In short: A delegate is a type-safe reference to one or more methods; an event wraps a delegate so outside code can only subscribe and unsubscribe, while only the declaring class can raise it.

    A delegate type such as Action<int> can hold several methods, a multicast delegate, and invoking it calls each in turn, as the two handlers below show. Exposed as a public field, though, any code could invoke it or overwrite the whole list with =, silently removing other subscribers. Declaring it as an event keeps the same delegate underneath but limits outside code to += and -=, so raising the event, and resetting its handlers, stays inside the declaring class, which is why the assignment below does not compile. That is the publisher-subscriber pattern behind UI events and notifications. A subscribed handler keeps its object alive for as long as the publisher lives, a common memory leak when handlers are never removed.

    var btn = new Button();
    btn.Clicked += n =>
        Console.WriteLine(n);
    btn.Clicked += n =>
        Console.WriteLine(n * 10);
    btn.Clicked = null;
    // does not compile
    btn.Press();
    
    class Button
    {
        public event Action<int>?
            Clicked;
        public void Press() =>
            Clicked?.Invoke(7);
    }
    // 7
    // 70
  7. 7.What is a closure in C#, and what does a lambda capture?

    In short: A lambda that uses a local variable captures the variable itself, not its current value, so it sees later changes and keeps the variable alive after the method returns.

    When a lambda refers to a local variable or parameter of the enclosing method, the compiler moves that variable into a hidden class shared by the method and the lambda, so both work on the same variable: changes made after the lambda is created are visible when it runs, and the variable lives as long as the lambda does, which is how the counter below keeps its count between calls. Each call to the enclosing method creates a new instance of that hidden class, so separate calls get separate variables. Capturing has a cost, an allocation per call, and static lambdas, from C# 9, forbid captures entirely so that none can happen by accident.

    var next = MakeCounter();
    next();
    next();
    Console.WriteLine(next());
    
    static Func<int> MakeCounter()
    {
        int count = 0;
        return () => ++count;
    }
    // 3

How the diagnostic asks it

One question from the C# bank, exactly as a sitting would show it. The bank has 4 on linq & lambdas and 30 across C#.

LINQ & Lambdas · mediumCSHARP-024

What does this C# code print?

var list = new List<int> { 1, 2, 3 };
var big = list.Where(x => x > 1);
list.Add(4);
Console.WriteLine(big.Count());
  1. 13correct
  2. 22
  3. 34
  4. 41

Where does not filter anything when it is called: it returns a query that remembers the source and the condition, and the filtering happens only when the query is enumerated, here by Count(). By then the list holds 1, 2, 3 and 4, so three elements pass: 3. 2 assumes Where filtered the list immediately, when it held 1, 2 and 3. 4 counts every element, ignoring the filter, and 1 counts only the added element. Calling ToList() or ToArray() runs a query at once and keeps the result; without it, every enumeration runs the query again against the current data.

Measure it

Reading answers tells you what’s true. A diagnostic tells you what you get wrong.

10 C# questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.

What the readiness test measures · how the score is computed

By Harshit · updated