Повторное объявление локальной переменной в новом потоке

Может быть это выглядит логично с точки зрения объявления переменной в новом потоке. Но даже в лямбда-функциях невозможно объявить одноимённую переменную, если она была объявлена выше.

Можно ли это считать недочётом самого языка программирования или я в чём-то ошибаюсь?

static void Main(string[] args)
    {
        var bag = new ConcurrentBag<int>();

        var threadCollection = new List<Thread>();


        for (int i_index = 0; i_index < 5; ++i_index)
        {
            new Thread(() =>
            {
                Thread.CurrentThread.Name = i_index.ToString();
                Console.WriteLine(Thread.CurrentThread.Name);

                for (int i_index = 0; i_index < 100; ++i_index)
                {
                    Thread.Sleep(75);
                    bag.Add(i_index);
                }

                Console.WriteLine(Thread.CurrentThread.Name + "ended");
            }).Start();
        }

        Console.Read();

        foreach (var item in bag)
        {
            Console.Write($"{item} ");
        }

        Console.WriteLine(bag.Count);
    }

Ответы (2 шт):

Автор решения: PashaPash

Если строго по спецификации: то в рамках анонимной функции i - это outer variable, а не local variable, так что разрешено объявлять local variable с тем же именем:

7.15.5 Outer variables
Any local variable, value parameter, or parameter array whose scope includes the lambda-expression or anonymous-method-expression is called an outer variable of the anonymous function.


Потоки тут особо ни при чем, просто внезапно с точки зрения анонимной функции локальная переменная того метода, в котором она объявлена, не является локальной переменной в этой самой анонимной функции. Это же касается и лямбд при компиляции их в анонимные функции:

static void Some()
{
    int i = 1;
    Action a = () => { // у вас коде этот Action отдается в конструктор Thread
        int i = 2; // нет ошибки, 
    };
}

Компилятор разворачивает такую конструкцию во что-то типа:

class __Locals1
{
    public int x; 
    public void __Method1() {
        int x = 2;
    }
}

static void Some()
{
    __Locals1 __locals1 = new __Locals1();
    __locals1.x = 1;
    Action a = new Action(__locals1.__Method1);
}

А объявлять переменные с тем же именем, что поля класса - можно. Но фишка интересная, до вашего вопроса я с ней не сталкивался :)

EDIT: По поводу обращения к i до ее объявления в теле цикла - да, в рамках scope локальной переменной нельзя обращаться к ней до объявления, for в примере просто ограничивает scope:

static void Some()
{
    int i = 1;
    Action a = () => { // вместо Thread, потоки ни при чем
        i = 2; // обращение к outer variable
        { int i = 2; } // вместо for, scope для local variable i - от скобки до скобки
    };
}

без скобок упадет:

static void Some()
{
    int i = 1;
    Action a = () => { // вместо Thread, потоки ни при чем
        i = 2; // обращение к local variable i в ее scope до объявления
        int i = 2; // scope от скобки до скобки
    };
}
→ Ссылка
Автор решения: Blackmeser

Можно объявлять, недочётом не является.
Могу объяснить тем, что у потока свой стек, анализатор это учитывает и разрешает имена.
Сам часто так делаю.
Внутренний int i_index у него локальный, а внешний int i_index он получает по ссылке, например в момент i_index.ToString(); Но, ни в коем случае не пишите так, как именно вы это сделали, потому-что если у вас будет небольшая задержка в выполнении, например:

Thread.Sleep(200);
Thread.CurrentThread.Name = i_index.ToString();

То вывод изменится:

Ожидание   Реальность
1          5
2          5
3          5
4          5
5          5
4ended     5ended
3ended     5ended
5ended     5ended
2ended     5ended
1ended     5ended

Чтобы всё работало корректно, нужно копировать значение итератора для потока:

for (int i_index = 0; i_index < 5; ++i_index)
{
    int i_index_copy = i_index; //желательно делать в первую очередь
    new Thread(() =>
    {
        Thread.Sleep(200);
        Thread.CurrentThread.Name = i_index_copy.ToString();
        Console.WriteLine(Thread.CurrentThread.Name);

        for (int i_index = 0; i_index < 100; ++i_index)
        {
            Thread.Sleep(75);
            bag.Add(i_index);
        }

        Console.WriteLine(Thread.CurrentThread.Name + "ended");
    }).Start();
}

Но, т.к. вы делали ++i_index вам нужно учесть смещение, а вывод будет следующим:

Ожидание   Реальность   Новый код
1          5            1
2          5            0
3          5            2
4          5            3
5          5            4
4ended     5ended       4ended
3ended     5ended       0ended
5ended     5ended       1ended
2ended     5ended       3ended
1ended     5ended       2ended
→ Ссылка