Ни компилятор, ни среда выполнения не обязаны гарантировать, что для локальных компьютеров, находящихся вне области действия, время жизни их содержимого будет сокращено. Для компилятора или среды выполнения вполне законно рассматривать это так, как если бы в целях продолжительности вычислений не было фигурных скобок. Если вам требуется очистка на основе фигурных скобок, то внедрите IDisposable и используйте блок «using».
UPDATE:
Относительно вашего вопроса "почему это отличается в оптимизированных и неоптимизированных сборках", хорошо, посмотрите на разницу в codegen.
неоптимизированному:
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 28 (0x1c)
.maxstack 1
.locals init (class test.Foo V_0)
IL_0000: nop
IL_0001: nop
IL_0002: newobj instance void test.Foo::.ctor()
IL_0007: stloc.0
IL_0008: nop
IL_0009: call void [mscorlib]System.GC::Collect()
IL_000e: nop
IL_000f: call void [mscorlib]System.GC::WaitForPendingFinalizers()
IL_0014: nop
IL_0015: call string [mscorlib]System.Console::ReadLine()
IL_001a: pop
IL_001b: ret
} // end of method Program::Main
Оптимизированный:
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 23 (0x17)
.maxstack 8
IL_0000: newobj instance void test.Foo::.ctor()
IL_0005: pop
IL_0006: call void [mscorlib]System.GC::Collect()
IL_000b: call void [mscorlib]System.GC::WaitForPendingFinalizers()
IL_0010: call string [mscorlib]System.Console::ReadLine()
IL_0015: pop
IL_0016: ret
} // end of method Program::Main
Очевидно, огромная разница. Очевидно, что в неоптимизированной сборке ссылка хранится в нулевом локальном слоте и никогда не удаляется, пока метод не завершится. Поэтому GC не может восстановить память, пока метод не завершится. В оптимизированной сборке ссылка хранится в стеке, сразу же выталкивается из стека, и GC может ее восстановить, поскольку в стеке не осталось действительной ссылки.