Как заставить C # Switch использовать IgnoreCase

89

Если у меня есть оператор switch-case, в котором объект в переключателе является строкой, можно ли выполнить сравнение ignoreCase?

Например, у меня есть:

string s = "house";
switch (s)
{
  case "houSe": s = "window";
}

Получится sзначение «окно»? Как мне переопределить оператор switch-case, чтобы он сравнивал строки с помощью ignoreCase?

Толсан
источник

Ответы:

63

Как вам кажется, вы знаете, что нижний регистр двух строк и их сравнение - это не то же самое, что сравнение без учета регистра. На то есть масса причин. Например, стандарт Unicode позволяет кодировать текст с диакритическими знаками несколькими способами. Некоторые символы включают в себя как основной символ, так и диакритический знак в одной кодовой точке. Эти символы также могут быть представлены как основной символ, за которым следует комбинированный диакритический знак. Эти два представления одинаковы для всех целей, и сравнение строк с учетом языка и региональных параметров в .NET Framework правильно идентифицирует их как равные либо с CurrentCulture, либо с InvariantCulture (с IgnoreCase или без него). С другой стороны, порядковое сравнение неправильно расценивает их как неравные.

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

То, что я делал в прошлом, чтобы добиться правильного поведения, - это просто смоделировать свой собственный оператор switch. Есть много способов сделать это. Один из способов - создать List<T>пару строк case и делегатов. В списке можно искать, используя правильное сравнение строк. Когда совпадение найдено, может быть вызван связанный делегат.

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

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

Если есть много случаев для сравнения и производительность является проблемой, то List<T>описанный выше параметр можно заменить отсортированным словарем или хеш-таблицей. Тогда производительность может потенциально соответствовать или превышать параметр оператора switch.

Вот пример списка делегатов:

delegate void CustomSwitchDestination();
List<KeyValuePair<string, CustomSwitchDestination>> customSwitchList;
CustomSwitchDestination defaultSwitchDestination = new CustomSwitchDestination(NoMatchFound);
void CustomSwitch(string value)
{
    foreach (var switchOption in customSwitchList)
        if (switchOption.Key.Equals(value, StringComparison.InvariantCultureIgnoreCase))
        {
            switchOption.Value.Invoke();
            return;
        }
    defaultSwitchDestination.Invoke();
}

Конечно, вы, вероятно, захотите добавить некоторые стандартные параметры и, возможно, тип возвращаемого значения в делегат CustomSwitchDestination. И вам захочется придумать лучшие имена!

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

    if (s.Equals("house", StringComparison.InvariantCultureIgnoreCase))
    {
        s = "window";
    }
    else if (s.Equals("business", StringComparison.InvariantCultureIgnoreCase))
    {
        s = "really big window";
    }
    else if (s.Equals("school", StringComparison.InvariantCultureIgnoreCase))
    {
        s = "broken window";
    }
Джеффри Л. Уитледж
источник
6
Если я не ошибаюсь, они отличаются только для определенных культур (например, турецкой), и в этом случае он не мог использовать ToUpperInvariant()или ToLowerInvariant()? Кроме того, он не сравнивает две неизвестные строки , он сравнивает одну неизвестную строку с одной известной строкой. Таким образом, пока он знает, как жестко закодировать подходящее представление в верхнем или нижнем регистре, блок переключателя должен работать нормально.
Сет Петри-Джонсон,
8
@Seth Petry-Johnson - Возможно, такую ​​оптимизацию можно провести, но причина, по которой параметры сравнения строк встроены в структуру, заключается в том, что нам всем не нужно становиться экспертами по лингвистике, чтобы писать правильное расширяемое программное обеспечение.
Джеффри Л. Уитледж,
54
ОК. Я приведу пример, где это уместно. Предположим, что вместо «дом» у нас было (по-английски!) Слово «кафе». Это значение может быть одинаково хорошо (и с равной вероятностью) представлено как "caf \ u00E9", так и "cafe \ u0301". Порядковое равенство (как в операторе switch) с любым ToLower()или ToLowerInvariant()вернет false. Equalswith StringComparison.InvariantCultureIgnoreCaseвернет истину. Поскольку обе последовательности выглядят одинаково при отображении, ToLower()версия представляет собой неприятную ошибку, которую нужно отслеживать. Вот почему всегда лучше проводить правильные сравнения строк, даже если вы не турок.
Джеффри Л. Уитледж,
77

Более простой подход - просто уменьшить регистр вашей строки до того, как она войдет в оператор switch, и сделать регистр ниже.

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

Например:

string s = "house"; 
switch (s.ToLower()) { 
  case "house": 
    s = "window"; 
    break;
}
Ник Крейвер
источник
1
Да, я понимаю, что нижний регистр - это способ, но я хочу, чтобы он игнорировался. Есть ли способ переопределить оператор switch-case?
Толсан
6
@Lazarus - это из CLR через C #, он был размещен здесь некоторое время назад в потоке скрытых функций: stackoverflow.com/questions/9033/hidden-features-of-c/… Вы можете запустить LinqPad с помощью нескольких миллион итераций, верно.
Ник Крейвер
1
@Tolsan - Нет, к сожалению, не только из-за статичности. Некоторое
Ник Крейвер
9
Похоже, что ToUpper(Invariant)это не только быстрее, но и надежнее: stackoverflow.com/a/2801521/67824
Охад Шнайдер
3
8 лет спустя ... twitter.com/Nick_Craver/status/970736005287264256
fubo 08
47

Приносим извинения за это новое сообщение по старому вопросу, но есть новый вариант решения этой проблемы с использованием C # 7 (VS 2017).

C # 7 теперь предлагает «сопоставление с образцом», и его можно использовать для решения этой проблемы следующим образом:

string houseName = "house";  // value to be tested, ignoring case
string windowName;   // switch block will set value here

switch (true)
{
    case bool b when houseName.Equals("MyHouse", StringComparison.InvariantCultureIgnoreCase): 
        windowName = "MyWindow";
        break;
    case bool b when houseName.Equals("YourHouse", StringComparison.InvariantCultureIgnoreCase): 
        windowName = "YourWindow";
        break;
    case bool b when houseName.Equals("House", StringComparison.InvariantCultureIgnoreCase): 
        windowName = "Window";
        break;
    default:
        windowName = null;
        break;
}

Это решение также касается проблемы, упомянутой в ответе @Jeffrey L Whitledge, что сравнение строк без учета регистра не то же самое, что сравнение двух строк с нижним регистром.

Кстати, в феврале 2017 года в журнале Visual Studio Magazine была интересная статья, в которой описывалось сопоставление с образцом и его использование в блоках case. Пожалуйста, взгляните: Сопоставление с образцом в блоках регистра C # 7.0

РЕДАКТИРОВАТЬ

В свете ответа @LewisM важно отметить, что у switchоператора есть новое интересное поведение. То есть, если ваш caseоператор содержит объявление переменной, то значение, указанное в switchчасти, копируется в переменную, объявленную в case. В следующем примере значение trueкопируется в локальную переменную b. Кроме того, переменная bне используется и существует только для того when, чтобы caseмогло существовать предложение к оператору:

switch(true)
{
    case bool b when houseName.Equals("X", StringComparison.InvariantCultureIgnoreCase):
        windowName = "X-Window";):
        break;
}

Как указывает @LewisM, это можно использовать с пользой - это преимущество состоит в том, что сравниваемая вещь фактически находится в switchоператоре, как и при классическом использовании switchоператора. Кроме того, временные значения, объявленные в caseоператоре, могут предотвратить нежелательные или непреднамеренные изменения исходного значения:

switch(houseName)
{
    case string hn when hn.Equals("X", StringComparison.InvariantCultureIgnoreCase):
        windowName = "X-Window";
        break;
}
STLDev
источник
2
Это было бы дольше, но я бы предпочел switch (houseName)провести сравнение так же, как и вы, то естьcase var name when name.Equals("MyHouse", ...
LewisM 07
@LewisM - Это интересно. Вы можете показать рабочий пример?
STLDev
@LewisM - отличный ответ. Я добавил дальнейшее обсуждение присвоения switchзначений аргументов caseвременным переменным.
STLDev
Ура для сопоставления с образцом в современном C #
Тьяго Силва
Вы также можете использовать «сопоставление с образцом объекта», case { } whenчтобы не беспокоиться о типе и имени переменной.
Боб
32

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

SampleEnum Result;
bool Success = SampleEnum.TryParse(inputText, true, out Result);
if(!Success){
     //value was not in the enum values
}else{
   switch (Result) {
      case SampleEnum.Value1:
      break;
      case SampleEnum.Value2:
      break;
      default:
      //do default behaviour
      break;
   }
}
uli78
источник
Просто примечание: Enum TryParse, похоже, доступен с Framework 4.0 и более поздними версиями, к вашему сведению. msdn.microsoft.com/en-us/library/dd991317(v=vs.100).aspx
granadaCoder
4
Я предпочитаю это решение, поскольку оно не рекомендует использовать магические строки.
user1069816
21

Расширение ответа @STLDeveloperA. Новый способ оценки операторов без использования нескольких операторов if в C # 7 - это использование оператора Switch сопоставления с шаблоном, аналогично тому, как @STLDeveloper, хотя этот способ включает переключаемую переменную.

string houseName = "house";  // value to be tested
string s;
switch (houseName)
{
    case var name when string.Equals(name, "Bungalow", StringComparison.InvariantCultureIgnoreCase): 
        s = "Single glazed";
    break;

    case var name when string.Equals(name, "Church", StringComparison.InvariantCultureIgnoreCase):
        s = "Stained glass";
        break;
        ...
    default:
        s = "No windows (cold or dark)";
        break;
}

В журнале visual studio есть хорошая статья о блоках регистров с сопоставлением с образцом, на которую стоит обратить внимание.

LewisM
источник
Спасибо, что указали на дополнительную функциональность нового switchзаявления.
STLDev
5
+1 - это должен быть принятый ответ для современной разработки (начиная с C # 7). Одно изменение, которое я хотел бы сделать, заключается в том, что я бы case var name when "Bungalow".Equals(name, StringComparison.InvariantCultureIgnoreCase):написал такой код: так как это может предотвратить исключение нулевой ссылки (где houseName имеет значение null) или, в качестве альтернативы, добавить случай, когда строка сначала будет нулевой.
Джей
19

Один из возможных способов - использовать словарь игнорирования регистра с делегатом действия.

string s = null;
var dic = new Dictionary<string, Action>(StringComparer.CurrentCultureIgnoreCase)
{
    {"house",  () => s = "window"},
    {"house2", () => s = "window2"}
};

dic["HouSe"]();

// Обратите внимание, что вызов не возвращает текст, а только заполняет локальную переменную s.
// Если вы хотите вернуть реальный текст, заменить Actionна Func<string>и значение в словаре , чтобы что - то вроде() => "window2"

Магнус
источник
4
Скорее CurrentCultureIgnoreCase, OrdinalIgnoreCaseпредпочтительнее.
Ричард Ev
2
@richardEverett Предпочитаете? Зависит от того, что вы хотите, если вы хотите, чтобы текущая культура игнорировала регистр, это нежелательно.
Магнус
Если кому-то интересно, мое решение (ниже) берет эту идею и превращает ее в простой класс.
Flydog57
2

Вот решение, которое объединяет решение @Magnus в класс:

public class SwitchCaseIndependent : IEnumerable<KeyValuePair<string, Action>>
{
    private readonly Dictionary<string, Action> _cases = new Dictionary<string, Action>(StringComparer.OrdinalIgnoreCase);

    public void Add(string theCase, Action theResult)
    {
        _cases.Add(theCase, theResult);
    }

    public Action this[string whichCase]
    {
        get
        {
            if (!_cases.ContainsKey(whichCase))
            {
                throw new ArgumentException($"Error in SwitchCaseIndependent, \"{whichCase}\" is not a valid option");
            }
            //otherwise
            return _cases[whichCase];
        }
    }

    public IEnumerator<KeyValuePair<string, Action>> GetEnumerator()
    {
        return _cases.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _cases.GetEnumerator();
    }
}

Вот пример его использования в простом приложении Windows Form:

   var mySwitch = new SwitchCaseIndependent
   {
       {"hello", () => MessageBox.Show("hello")},
       {"Goodbye", () => MessageBox.Show("Goodbye")},
       {"SoLong", () => MessageBox.Show("SoLong")},
   };
   mySwitch["HELLO"]();

Если вы используете лямбды (как в примере), вы получите замыкания, которые будут захватывать ваши локальные переменные (довольно близко к ощущению, которое вы получаете от оператора switch).

Поскольку он использует словарь под прикрытием, он получает поведение O (1) и не полагается на просмотр списка строк. Конечно, вам нужно составить этот словарь, и это, вероятно, стоит дороже.

Вероятно, имеет смысл добавить простой bool ContainsCase(string aCase) метод, который просто вызывает метод словаря ContainsKey.

Flydog57
источник
1

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

public string ConvertMeasurements(string unitType, string value)
{
    switch (unitType.ToLower())
    {
        case "mmol/l": return (Double.Parse(value) * 0.0555).ToString();
        case "mg/dl": return (double.Parse(value) * 18.0182).ToString();
    }
}
UnknownFellowCoder
источник
0

Для этого должно быть достаточно:

string s = "houSe";
switch (s.ToLowerInvariant())
{
  case "house": s = "window";
  break;
}

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

Кевин Беннетт
источник