Почему я не могу передать большое количество ссылок на функцию?

Я хочу сделать что-то вроде этого:

double a, b, c, d, e;
ParseAndWrite("{1, 2, 3}", ref a, ref b, ref c);
ParseAndWrite("{4, 5}", ref d, ref e);

-> a = 1, b = 2, c = 3, d = 4, e = 5

Однако я не могу написать такую ​​функцию:

private void ParseAndWrite(string leInput, params ref double[] targets)
{
   (...)
}

Это не работает, по какой-то причине нельзя использовать ref и params одновременно. Почему так?

edit: ОК, вот еще информация о том, зачем мне это нужно: через интерфейс я получаю много строк, содержащих значения, с синтаксисом вроде:

inputConfig : " step, stepHeight, rStep, rStepHeight, (nIterations, split, smooth) "
outputConfig : " dataSelection, (corrected, units, outlierCount, restoreOriginalRange) "

(имена в скобках необязательны). Эти значения должны анализироваться и сохраняться во всех конкретных переменных. То есть - они вовсе не массивы. Они больше похожи на аргументы командной строки, но около 20 из них. Я могу, конечно, сделать все это последовательно, но это создает сотни строк кода, которые содержат избыточный шаблон и не очень удобны в обслуживании.

Ответ 1

по какой-то причине нельзя использовать ref и params одновременно. Почему так?

Рассмотрим эти три желательные характеристики камер:

  • легкий

  • высококачественный объектив

  • недороги

У вас должно быть не более двух из них в любой камере. Вы никогда не получите все три. Вы можете приобрести недорогие тяжелые камеры с большими объективами или дорогие легкие камеры с большими объективами или недорогие легкие фотокамеры, но нет недорогих, легких камер с большими объективами.

Вернемся к вашему вопросу. Рассмотрим эти три желательные характеристики среды выполнения:

  • param-массивы типов ref-to-variable являются законными

    Система
  • безопасна

  • локальные переменные, у которых есть ссылки на них, по-прежнему быстро выделяют и освобождают

У вас есть два, но у вас не может быть всех троих. Которые вам бы хотели?

Вы можете иметь ref param массивы и систему безопасного типа за счет локальных переменных ref'd, выделяемых на собранную кучу мусора. Насколько мне известно, никто этого не делает, но это, безусловно, возможно.

У вас могут быть ref param массивы, все локальные переменные, выделенные во временном пуле, и система типов, которая сбой при неправильном использовании. Это подход "C/С++"; можно взять ссылку и сохранить ее в месте, которое имеет более длительный срок службы, чем срок службы вещи, на которую ссылаются. Если вы это сделаете, вы можете ожидать, что ваша программа на С++ выйдет из строя и умрет самым ужасным образом, сделав легкие ошибки, которые трудно обнаружить. Добро пожаловать в яму отчаяния.

Или мы можем сделать ref param arrays незаконными, выделить все локальные переменные во временном пуле и иметь систему типов, которая достоверно безопасна для памяти. Это выбор, который мы сделали при построении С# и CLR; добро пожаловать в яму качества.


Теперь то, что я сказал выше, на самом деле является большой ложью. Это ложь, потому что среда выполнения и язык С# действительно поддерживают функцию, сродни (но не идентичную) массивам параметров ref. Это приятная ложь и поверьте мне, вы хотите жить в мире, где вы верите в ложь, поэтому я рекомендую вам взять голубую таблетку и продолжать это делать.

Если вы хотите взять красную таблетку и выяснить, насколько глубока лунка кролика, вы можете использовать недокументированную функцию __arglist для С# для создания вариабельного метода C-стиля, а затем make TypedReference объекты, которые ссылаются на ссылки произвольных полей или локалей типов структур, а затем передают произвольно многие из них в вариационный метод. Используйте либо методы factory TypedReference, либо недокументированную функцию __makeref на С#.

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

Правильное использование объектов TypedReference не для слабонервных, и я настоятельно рекомендую против этого. Я никогда не делал этого в производственном кодексе за многие годы написания С#. Если вы хотите передать ссылки на произвольно многие переменные, передайте массив. Массив по определению представляет собой набор переменных. Это гораздо безопаснее, чем прохождение вокруг множества объектов, представляющих управляемые адреса экземпляров или локальных переменных.

Ответ 2

Там много вопросов "почему не язык X делает Y", о котором можно было бы спросить. Чаще всего ответ заключается в том, что, вероятно, не было большой потребности в этой функции.

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

Вы можете передать предварительный размер double[] для метода для заполнения, конечно, хотя мое предпочтение состоит в том, чтобы просто вернуть IEnumerable<double>

Ответ 3

Я не знаю, является ли это единственной причиной, но вот одно из возможных объяснений:

Фактический параметр по-прежнему в конечном счете равен double[], поэтому для того, чтобы делать то, что вы хотите, компилятору действительно нужно было бы написать это как:

{
    double[] arr = new double[] {a, b, c, d}; // copy in
    YourMethod(arr);
    a = arr[0]; // copy back out
    b = arr[1];
    c = arr[2];
    d = arr[3];
}

Однако! это изменяет семантику. Представьте, что метод вызова был фактически:

void SomeCallingMethod(ref double a)
{
    double b = ..., c = ... d = ...;
    YourMethod(ref a, ref b, ref c, ref d);
    /*  which, say, is translated to
    {
        double[] arr = new double[] {a, b, c, d}; // copy in
        YourMethod(arr);
        a = arr[0]; // copy back out
        b = arr[1];
        c = arr[2];
        d = arr[3];
    }  */      
}

Ожидаемое поведение заключается в том, что мы постоянно обновляем a, но на самом деле мы не. Это может привести к странным и запутанным ошибкам. Действительно, чтобы сделать это правильно, "копировать назад" нужно было бы перейти в finally, так как фактически ref значения должны показывать изменения до исключения, но это все еще не изменяет семантику обновления после метод вызывает, а не во время.

Вы также задаетесь очень странным вопросом: сколько времени обновляет массив, чтобы обновить вызывающего? в конце концов, массив можно хранить в любом месте, так как он не находится в стеке.


Есть другой способ отправить это как массив ссылок на значения, но это может быть действительно смертельно:

  • он сделает код по существу unsafe
  • Если массив хранился где-нибудь (какие могут быть массивы), вызывающий метод мог бы возвратить и удалить областью, которая использовалась, что означает, что будет действительная ссылка, которая фактически указывает на мусор - Ahoy

В целом, из этого ничего хорошего не получается, и много плохих вещей.

Ответ 4

Причина, вероятно, техническая. "params" - это просто синтаксический сахар для аргумента массива. Поэтому ParseAndWrite(string leInput, params double[] targets) скомпилирован как ParseAndWrite(string leInput, params double[] targets).

Затем ParseAndWrite(string leInput, params ref double[] targets) будет скомпилирован как ParseAndWrite(string leInput, ref double[] targets). В принципе, это будет массив, который передается как ссылка, а не фактическое содержимое массива. Очевидно, что это не будет поведение, ожидаемое разработчиками, и поэтому будет подвержено ошибкам.