Это хорошо известное и устоявшееся поведение в отношении лямбд, хотя оно часто удивляет тех, кто столкнулся с ним впервые. Основная проблема заключается в том, что ваша ментальная модель того, что такое лямбда , не совсем верна.
Лямбда - это функция, которая не запускается, пока не будет вызвана. Ваше закрытие связывает ссылку на этот лямбда-экземпляр, а не значение. Когда вы выполняете свои действия в последнем цикле foreach, вы впервые переходите по закрытой ссылке, чтобы увидеть, что это такое.
В первом случае вы ссылаетесь на num, и в этот момент значение num равно 4, поэтому, конечно, все ваши выходные данные равны 4. Во втором случае каждая лямбда-выражение было связано с различным значением, которое каждый раз был локальным для цикла, и это значение не изменялось (оно не было GC-ом исключительно из-за лямбда-ссылки.) поэтому вы получите ожидаемый ответ.
Закрытие по локальному временному значению на самом деле является стандартным подходом для захвата определенного значения с момента времени в лямбда-выражении.
Ссылка Адама на блог Эрика Липперта дает более глубокое (и технически точное) описание происходящего.