Законно ли писать программу из процедур без in/out параметров, которые оперируют глобальными переменными?

Столкнулся с программой написанной на мой неопытный взгляд в странном стиле, а именно:

Почти все действия выполняет большое количество процедур не умеющих IN/OUT параметров. При этом они оперируют глобальными параметрами. В программе уже 4200 строк.

Упрощено программу можно представить следующим образом:

int a;
int b;
int result;
int c = 6;
void Click1() { 
   proc1();
   proc2();
   proc3(); 
}
void Click2() { 
   proc1();
   proc2();
   proc4(); 
}
void proc1() {a = 3;}
void proc2() {b = 5;}
void proc3() {result = a + b + c; Console.Write(result)}
void proc4() {result = a * b / c; Console.Write(result)}

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

На мой взгляд, программа должна выглядеть следующим образом:

int C = 6;
void Click1() { 
   proc1(int out a);
   proc2(int out b);
   proc3(a, b, C);
}
void Click2() { 
   proc1(int out a);
   proc2(int out b);
   proc4(a, b, C); 
}
void proc1(int out a) {a = 3;}
void proc2(int out b) {b = 5;}
void proc3(int a, int b, C) {
    int result;
    result = a + b + С;
    Console.Write(result)
}
void proc4(int a, int b, C) {
    int result;
    result = a * b / С;
    Console.Write(result)
}

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

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

В C# не существует глобальных переменных, так что нет.

→ Ссылка
Автор решения: Виталий Злобин

Лучше не использовать глобальные переменные - их могут изменить и нет способа это узнать. В целом про глобальные переменные много статей, где сказано, что от них много проблем, хотя в начале проекта есть соблазн их использовать. Допускается аккуратно использовать глобальные константы(возможно, есть смысл хранить их в конфиг файле)

→ Ссылка
Автор решения: aepot

Улучшает ли ситуацию второй вариант или как написал коментатор выше, второй код ничем не лучше? И что тогда с ним не так?

out здесь вообще не нужен. Этот пример кода был непрезентабелен. Привести его в порядок можно например вот так:

int C = 6;

void Click1()
{ 
   int a = proc1();
   int b = proc2();
   proc3(a, b);
}

void Click2()
{ 
   int a = proc1();
   int b = proc2();
   proc4(a, b); 
}

int proc1() { return 3; }
int proc2() { return 5; }

void proc3(int a, int b)
{
    int result = a + b + C;
    Console.Write(result)
}

void proc4(int a, int b)
{
    int result = a * b / C;
    Console.Write(result)
}

Использовать поля класса в методах можно и даже нужно, но с умом. Чем больше логическая изоляция блоков кода, тем легче будет потом это всё переписывать, улучшать и т.д.

А понятия "глобальные переменные" в C# нет, есть только локальные.


По поводу реальных примеров out, то есть 2 варианта логики - когда возвращаемые результаты логически нужны для разных целей, и когда они являются однородной группой.

Например метод попытки целочисленного деления, чтобы избежать исключения. Один результат метода требуется для условной конструкции, второй для работы с вычисленным результатом, то есть в разных местах.

bool TryDivide(int a, int b, out int result)
{
    if (b != 0)
    {
        result = a / b;
        return true;
    }
    result = default;
    return false;
}

if (TryDivide(6, 3, out int number))
{
    Console.WriteLine(number);
}
else
{
    Console.WriteLine("деление не удалось");
}

Кстати, про то, как правильно работать с исключениями, я писал здесь.

А когда результат - группа значений, их можно объединить в класс, структуру или кортеж и вернуть через возвращаемое значение.

class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

Point GetPoint(int x, int y)
{
    return new Point(x, y);
}

Point p = GetPoint(1, 2);
Console.WriteLine(p.X);
Console.WriteLine(p.Y);

Это всего-лишь примеры хорошего кода, на практике автор приложения - вы и вам решать, как правильно писать код. Стоит только учитывать несколько факторов:

  • сможет ли его без труда прочитать кто-то другой, если вы его покажете
  • сможете ли вы его сами прочитать спустя какое-то время (например через полгода)
  • сможете ли вы его потом дорабатывать не переписывая весь код сначала

Есть плотно связанные с этим принципы ООП SOLID, до них я вам тоже советую со временем добраться.


По заявкам телезрителей, вот пример с кортежем

(int, int, int) GetNumbers()
{
    return (1, 2, 3);
}

(int a, int b, int c) = GetNumbers();
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
→ Ссылка
Автор решения: CrazyElf

Самая главная проблема функций, написанных без явной передачи параметров и имеющих "побочные эффекты" - это то, что их непонятно как тестировать. Функции, которые получают все параметры на вход в явном виде и возвращают результат в явном виде, легко тестировать и можно быть уверенным (до какой-то степени), что они работают так, как задумано, если они покрыты тестами.

Поэтому да, в идеале такой код нужно переписывать на нормальный. Вопрос в том, стоит ли пытаться рефакторить имеющийся код или быстрее/лучше будет написать код заново, сразу уже по-правильному.

→ Ссылка