Оператор переключения нескольких переменных в C#

Я хотел бы использовать оператор switch, который принимает несколько переменных и выглядит так:

switch (intVal1, strVal2, boolVal3)
{
   case 1, "hello", false:
      break;
   case 2, "world", false:
      break;
   case 2, "hello", false:

   etc ....
}

Есть ли способ сделать что-то подобное в C#? (Я не хочу использовать вложенные операторы switch по понятным причинам).

13 ответов

Решение

В C# нет встроенной функциональности, и я не знаю ни одной библиотеки, которая бы делала это.

Вот альтернативный подход, используя Tuple и методы расширения:

using System;

static class CompareTuple {
    public static bool Compare<T1, T2, T3>(this Tuple<T1, T2, T3> value, T1 v1, T2 v2, T3 v3) {
        return value.Item1.Equals(v1) && value.Item2.Equals(v2) && value.Item3.Equals(v3); 
    }
}

class Program {
    static void Main(string[] args) {
        var t = new Tuple<int, int, bool>(1, 2, false);
        if (t.Compare(1, 1, false)) {
            // 1st case
        } else if (t.Compare(1, 2, false)) {
            // 2nd case
        } else { 
            // default
        }
    }
}

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

Если вы хотите переключить несколько входов и вернуть значение, в C# 8 вы можете использовать шаблоны кортежей. Это форма выражения переключателя сопоставления с образцом, которая принимает несколько значений в качестве входных данных в форме кортежа. Вот пример использования ваших входных данных; обратите внимание на использование_ для случая по умолчанию:

string result = (intVal1, strVal2, boolVal3) switch
{
    (1, "hello", false) => "Combination1",
    (2, "world", false) => "Combination2",
    (2, "hello", false) => "Combination3",
    _ => "Default"
};

Вот более наглядный пример (игра " камень, ножницы, бумага") из статьи MSDN, указанной выше:

public static string RockPaperScissors(string first, string second)
    => (first, second) switch
    {
        ("rock", "paper") => "rock is covered by paper. Paper wins.",
        ("rock", "scissors") => "rock breaks scissors. Rock wins.",
        ("paper", "rock") => "paper covers rock. Paper wins.",
        ("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
        ("scissors", "rock") => "scissors is broken by rock. Rock wins.",
        ("scissors", "paper") => "scissors cuts paper. Scissors wins.",
        (_, _) => "tie"
    };

Вы можете сделать это в C# 7 и выше с помощью when ключевое слово:

switch (intVal1)
{
    case 1 when strVal2 == "hello" && boolVal3 == false:
        break;
    case 2 when strVal2 == "world" && boolVal3 == false:
        break;
    case 2 when strVal2 == "hello" && boolVal3 == false:
        break;
}

Давайте посмотрим на это по-другому. Если у вас есть:

  • Очень конкретные комбинации, которые вы хотите проверить;
  • Нет сравнения, чтобы сделать;
  • Обработчик по умолчанию для каждого несоответствующего регистра;
  • Все типы примитивов / значений (int, bool, string, так далее.)

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

Пример кода:

private static readonly Tuple<int, int, bool> NameOfCase1 = 
    Tuple.Create(1, 1, false);
private static readonly Tuple<int, int, bool> NameOfCase2 =
    Tuple.Create(2, 1, false);
private static readonly Tuple<int, int, bool> NameOfCase3 =
    Tuple.Create(2, 2, false);

private static readonly Dictionary<Tuple<int, int, bool>, string> Results =
    new Dictionary<Tuple<int, int, bool>, string>
{
    { NameOfCase1, "Result 1" },
    { NameOfCase2, "Result 2" },
    { NameOfCase3, "Result 3" }
};

public string GetResultForValues(int x, int y, bool b)
{
    const string defaultResult = "Unknown";
    var lookupValue = Tuple.Create(x, y, b);
    string result;
    Results.TryGetValue(lookupValue, out result);
    return defaultResult;
}

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

Обратите внимание, что я использую Tuple<T1,T2,T3> здесь, потому что в него уже встроена вся логика хеш-кода. Синтаксис немного неудобен в C#, но если вы хотите, вы можете реализовать свой собственный класс поиска и просто переопределить Equals а также GetHashCode,

Мой совершенно сумасшедший взгляд на это:

class Program
{
    static void Main(string[] args)
    {
        var i = 1;
        var j = 34;
        var k = true;
        Match(i, j, k).
            With(1, 2, false).Do(() => Console.WriteLine("1, 2, 3")).
            With(1, 34, false).Do(() => Console.WriteLine("1, 34, false")).
            With(x => i > 0, x => x < 100, x => x == true).Do(() => Console.WriteLine("1, 34, true"));

    }

    static Matcher<T1, T2, T3> Match<T1, T2, T3>(T1 t1, T2 t2, T3 t3)
    {
        return new Matcher<T1, T2, T3>(t1, t2, t3);
    }
}

public class Matcher<T1, T2, T3>
{
    private readonly object[] values;

    public object[] Values
    {
        get { return values; }
    }

    public Matcher(T1 t1, T2 t2, T3 t3)
    {
        values = new object[] { t1, t2, t3 };
    }

    public Match<T1, T2, T3> With(T1 t1, T2 t2, T3 t3)
    {
        return new Match<T1, T2, T3>(this, new object[] { t1, t2, t3 });
    }

    public Match<T1, T2, T3> With(Func<T1, bool> t1, Func<T2, bool> t2, Func<T3, bool> t3)
    {
        return new Match<T1, T2, T3>(this, t1, t2, t3);
    }
}

public class Match<T1, T2, T3>
{
    private readonly Matcher<T1, T2, T3> matcher;
    private readonly object[] matchedValues;
    private readonly Func<object[], bool> matcherF; 

    public Match(Matcher<T1, T2, T3> matcher, object[] matchedValues)
    {
        this.matcher = matcher;
        this.matchedValues = matchedValues;
    }

    public Match(Matcher<T1, T2, T3> matcher, Func<T1, bool> t1, Func<T2, bool> t2, Func<T3, bool> t3)
    {
        this.matcher = matcher;


        matcherF = objects => t1((T1)objects[0]) && t2((T2)objects[1]) && t3((T3)objects[2]);
    }

    public Matcher<T1, T2, T3> Do(Action a)
    {
        if(matcherF != null && matcherF(matcher.Values) || matcher.Values.SequenceEqual(matchedValues))
            a();

        return matcher;
    }
}

Вы можете преобразовать в строку:

switch (intVal1.ToString() + strVal2 + boolVal3.ToString())
{
   case "1helloFalse":
      break;
   case "2worldFalse":
      break;
   case "2helloFalse":

   etc ....
}

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

switch (first + last)
{
   case "ClarkKent":
   case "LoisLane":
      // YES
      break;
   default;
      // Sadly, no
      break;
}

Но что происходит, когда у тебя появляется другой парень по имени Кларк Кент? Неужели у вас не может быть другого значения, на основании которого вы определяете эту логику, например, bool KnowsSuperman?

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

Другим примером может быть, если вам нужно сгруппировать людей в несколько групп и выполнить некоторую логику в зависимости от группы, в которой они находятся. Вы можете написать код, чтобы сказать, что если вы Боб, Джефф, Джим или Салли, вы в группе А, но что, если вам нужно добавить кого-то еще в группу А? Вы должны изменить код. Вместо этого вы можете создать дополнительное свойство с именем Group, которое может быть перечислением или строкой, которые вы можете использовать, чтобы указать, в какую группу входит кто-то.

Я не уверен, в какой версии C# это появилось, но вы можете сделать это:

      var x = 22;
var y = 33;
var z = 44;

switch (x, y, z) {

    case (33, 33, 33):
        WriteLine("This should not run");
        break;

    case (22, 33, 44):
        WriteLine("This should run");
        break;
}

Обновление для 2018. Начиная с C#7.0, Microsoft ввела условие "когда" для коммутаторов, что позволяет эффективно расширять регистры коммутаторов дополнительными условиями.

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/switch

//.Net Core 3.1
    class Convertors
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Convertors.ConvertAny("m","cm", 10));
            Console.ReadKey();
        }
        public static double MToCM(double value)
        {
            return value * 100;
        }
        public static double ConvertAny(string srcUOM, string tgtUOM, double value)
        {
            switch (srcUOM.ToLower(), tgtUOM.ToLower())
            {
                case ("m", "cm"): return Convertors.MToCM(value);
                default: throw new NotImplementedException();
            }
        }
    }

Согласно спецификации языка C#, switch Выражение оператора должно иметь одно из следующих значений: sbyte, byte, sbyte, byte, short, ushort, int, uint, long, ulong, char, string или enum-type. Это означает, что вы не можете включить Tuple или другие типы более высокого порядка.

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

switch (intVal1 * 100 + intVal2 * 10 + (boolVal3 ? 1 : 0))
{
case 100: /* intVal1 = 1, intVal2 = 0, boolVal3 = false */ ... break;
case 831: /* intVal1 = 8, intVal2 = 3, boolVal3 = true */ ... break;
}

Я делаю такие вещи со списками или массивами. Если вы можете перечислить возможные условия (что вы, очевидно, можете, если вы хотите сделать многозначное переключение), то создайте таблицу поиска с ключом из нескольких частей и Action или же Func<T> в качестве значения.

Простая версия будет использовать Dictionary:

class LookupKey: IComparable<LookupKey>
{
    public int IntValue1 { get; private set; }
    public int IntValue2 { get; private set; }
    public bool BoolValue1 { get; private set; }
    public LookupKey(int i1, int i2, bool b1)
    {
        // assign values here
    }
    public int Compare(LookupKey k1, LookupKey k2)
    {
        return k1.IntValue1 == k2.IntValue1 &&
               k1.IntValue2 == k2.IntValue2 &&
               k1.BoolValue1 == k2.BoolValue1;
    }
    public int GetHashCode()
    {
        return (19 * IntValue1) + (1000003 * IntValue2) + (BoolValue1) ? 0 : 100000073;
    }
    // need to override Equals
}

И твой словарь:

static readonly Dictionary<LookupKey, Action<object>> LookupTable;

Затем вы можете заполнить словарь при запуске, и тогда поиск станет простым вопросом:

Action<object> MethodToCall;
if (LookupTable.TryGetValue(new LookupKey(i1, i2, b1), out MethodToCall)
    MethodToCall(theData);
else
    // default action if no match

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

if (a == 1 && b == 1) {}
else if (a == 1 && b == 2) {}
else if (a == 2 && b ==2) {}

Вы не можете сделать это в C#, насколько я знаю.

Но вы можете сделать это из MSDN:

В следующем примере показано, что переход от одной метки к другой разрешен для пустых меток:

 switch(n) 
        {
            case 1:
            case 2: 
            case 3: 
                Console.WriteLine("It's 1, 2, or 3.");
                break; 
        default: 
            Console.WriteLine("Not sure what it is.");
            break; 
        }
Другие вопросы по тегам