Эффективное выражение логической сетки 2x2 в коде - PullRequest
5 голосов
/ 27 августа 2010

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

Каждый отдельный результат будет выполнять некоторую комбинацию действий / функций X, Y или Z. Z принимает параметр в диапазоне от -1 до 1. Порядок их выполнения не важен.

Посмотрите на следующую логическую сетку. Старое значение - это крайний левый столбец меток, а новое значение - верхний ряд меток:

          New:
          0          !=0
          --------   -------
Old:  0 | nothing    Y, Z(1)
    !=0 | X, Z(-1)   X, Y    -- Z(0) is okay but not required for this quadrant

Что было бы хорошим способом представить это?

Я работаю в C #, но приму ответы на любом языке, так как на самом деле это не языковой вопрос - я могу перевести все что угодно.

Пример:

if (oldvalue == 0 && newvalue == 0) return;
if (oldvalue != 0) X();
if (newvalue != 0) Y();
Z(oldvalue != 0 ? -1 : 0 + newvalue != 0 ? 1 : 0);

Полагаю, это выглядит неплохо, но есть и другие способы сделать это.

int which = (oldvalue == 0 ? 0 : 1) + (newvalue == 0 ? 0 : 2)

switch (which) {
   case 1:
      X(); Z(-1);
      break;
   case 2:
      Y(); Z(1);
      break;
   case 3:
      X(); Y();
      break;
}

Это на самом деле немного более простой случай, чем то, с чем я имею дело. В случае, если oldvalue и newvalue отличны от нуля и равны друг другу, трактуйте newvalue, как если бы это было 0.

Не стесняйтесь отвечать как дано или с этим дополнительным ограничением. Есть еще немного, но я думаю, что это слишком много, чтобы начать с. Если после этого все будет интересно, я оставлю все остальное здесь или в новом вопросе.

Я предполагаю, что задаю вопрос, потому что я часто сталкиваюсь с этими элементами логической сетки, и они не всегда 2х2, иногда они немного больше. Приятно заметить, что я могу обрабатывать некоторые ответы целыми «полосами», например, замечая, что X выполняется каждый раз, когда oldvalue! = 0, но мне кажется, что я начинаю сталкиваться с шаблоном, который требует некоторой выразительной логики в более общем смысле, вместо того, чтобы кропотливо превращать его в заявления, если не в другое. Я имею в виду, было бы очень здорово, если бы я мог обеспечить своего рода сетку логики и позволить компилятору выяснить лучший способ справиться с этим.

Просто дикий мозговой штурм:

Conditions:
oldvalue == 0 ? 0 : 1
newvalue == 0 ? 0 : 2

Actions:
X = {false, true, false, true}
Y = {false, false, true, true}
Z(-1) = true where condition = 1
Z(1) = true where condition = 2

Какие у вас идеи? Я вознагражу любое материальное участие вообще.

Ответы [ 3 ]

10 голосов
/ 27 августа 2010

Давайте посмотрим на вашу проблему с другой точки зрения. При разработке тела кода я стараюсь применять следующий принцип:

  1. Сделайте это правильно.
  2. Проясни.
  3. Сделайте это лаконичным.
  4. Сделай это быстро. ... в таком порядке.

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

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

Вторым приоритетом является обеспечение того, чтобы в будущем , когда разработчик (включая первоначального автора!) Посмотрел на этот код, он мог сразу понять, что он делает. Чем сложнее (читай: fancy ) реализация, тем сложнее разработчику сразу понять, что делает код.

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

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

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

void YourMethod( int oldValue, int newValue )
{
    bool oldValueNonZero = oldValue != 0;
    bool newValueNonZero = newValue != 0;

    if( oldValueNonZero ) { X(); }
    if( newValueNonZero ) { Y(); }
    if( oldValueNonZero && newValueNonZero ) { Z(); }
}

Так почему же мне нравится эта конкретная реализация? Давайте разберемся с этим.

Сначала , обратите внимание, что я решил создать временное логическое значение, чтобы получить результат проверки старого / нового значения на предмет того, являются ли они ненулевыми. Получая эти значения, я избегаю выполнения вычислений более одного раза, а также делаю код более читабельным (см. Ниже).

Второе , выбрав описательные имена oldValueNonZero и newValueNonZero Я делаю реализацию четко указать мои ожидания. Это улучшает читаемость кода и четко передает мои намерения будущим разработчикам, которые должны его прочитать.

Третий , обратите внимание, что тело теста if() заключено в скобки { и } - это уменьшает вероятность того, что будущие изменения в реализации нарушат поведение - на например, случайно включив новый случай. Использование однострочного ifs - рецепт для будущих проблем.

Наконец , я не пытаюсь замкнуть сравнение и рано выйти из функции. Если производительность была чрезвычайно важна, ранний выход может быть полезным. Но в противном случае проще понять поведение метода, если существует только одна точка выхода (1) .

Этот код выполняет то, что написано в спецификации? Я так считаю.

Легко ли читать и понимать. По крайней мере, на мой взгляд, я бы сказал, да.

Является ли этот код наиболее компактным или изощренным способом короткого замыкания логики? Почти наверняка нет ... но, на мой взгляд, другие качества более чем восполняют это.

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

Вы указали, что иногда вы сталкиваетесь с похожими проблемами с «логической сеткой», но те, где число случаев более многочисленное. Проблемы такого рода могут усложняться по двум отдельным причинам:

  1. Количество значений, которые параметры могут принимать при увеличении - они могут принимать общий вид MxN.
  2. Количество измерений увеличивается - другими словами, в правилах есть еще переменные: MxNxOxP...xZ.

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

Мой ответ на общий случай ... это зависит. Если условия могут быть уменьшены до некоторого очень небольшого числа случаев, то крайне важно, чтобы логика if / else по-прежнему оставалась лучшим способом решения проблемы. Если число условий очень велико, то может иметь смысл использовать декларативный подход, при котором вы используете некую таблицу поиска или матрицу для кодирования случаев.

1> - Одним из распространенных исключений из принципа наличия только одной точки выхода из метода являются предварительные условия. Лучше избежать вложенности и перевернутой логики, сначала проверив все / любые предварительные условия и не выполнив (выйдя) из метода, если они нарушены.

2 голосов
/ 27 августа 2010

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

Примерно так:

private void Y() { }
private void X() {}
private void Z(int n) {}

private void example(int oldvalue, int newvalue)
{
    var array = new Action[2, 2];
    array[0, 0] = () => { };
    array[0, 1] = () => { Y(); Z(1); };
    array[1, 0] = () => { X(); Z(-1); };
    array[1, 1] = () => { X(); Y(); };

    // invoke
    array[oldvalue == 0 ? 0 : 1, newvalue == 0 ? 0 : 1]();
}

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

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

  1. оценивать oldvalue == 0
  2. оценивать newvalue == 0
  3. индекс в массив для получения делегата
  4. выполнить делегат для выполнения действия

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

1 голос
/ 04 сентября 2010

Дополнительное кредитное решение

Это очень интересная тема с отличным пониманием со стороны авторов. Я думаю, что на ваш вопрос ответили, поэтому я сосредоточусь на дополнительном кредите. Самое простое - обновить таблицу и применить ее к любому решению, которое вам больше нравится.

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

          New == 0 || New == Old    New != 0 && New != Old
          ----------------------   ------------------------
Old == 0 | nothing                  Y, Z(1)
Old != 0 | X, Z(-1)                 X, Y    -- Z(0) is okay but not required for this quadrant

Таким образом, в решении Дторпа мы заменили бы newValue == 0 на newValue == 0 || newValue == oldValue:

private void example(int oldvalue, int newvalue)
{
    var array = new Action[2, 2];
    array[0, 0] = () => { };
    array[0, 1] = () => { Y(); Z(1); };
    array[1, 0] = () => { X(); Z(-1); };
    array[1, 1] = () => { X(); Y(); };

    array[oldvalue == 0 ? 0 : 1, newValue == 0 || newValue == oldValue ? 0 : 1]();
}

В решении Л.Бушкина мы заменили бы newValue != 0 на newValue != 0 && newValue != oldValue и обновили бы имя соответствующей переменной, чтобы сохранить читабельность. Я также собираюсь убить бит кода:

void YourMethod( int oldValue, int newValue )
{
    bool oldValueNonZero = oldValue != 0;
    bool newValueDifferentAndNonZero = newValue != 0 && newValue != oldValue;
    int zCondition = 0;

    if( oldValueNonZero ) { X(); zCondition--;}
    if( newValueDifferentAndNonZero ) { Y(); zCondition++;}
    if( zCondition != 0 ) { Z(zCondition); }
}

Тад!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...