Jak sprawić, aby instrukcja C # Switch używała IgnoreCase

89

Jeśli mam instrukcję switch-case, w której obiekt w przełączniku jest ciągiem, czy można wykonać porównanie ignoreCase?

Mam na przykład:

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

Otrzyma swartość „okno”? Jak zastąpić instrukcję switch-case, aby porównywała ciągi przy użyciu ignoreCase?

Tolsan
źródło

Odpowiedzi:

63

Jak zdajesz się być świadomy, zmniejszanie dwóch liter i porównywanie ich nie jest tym samym, co porównywanie wielkości liter. Jest ku temu wiele powodów. Na przykład standard Unicode umożliwia kodowanie tekstu ze znakami diakrytycznymi na wiele sposobów. Niektóre znaki zawierają zarówno znak podstawowy, jak i znak diakrytyczny w jednym punkcie kodowym. Znaki te mogą być również przedstawiane jako znak podstawowy, po którym następuje łączący znak diakrytyczny. Te dwie reprezentacje są równe we wszystkich celach, a porównania ciągów uwzględniające kulturę w .NET Framework poprawnie zidentyfikują je jako równe, z CurrentCulture lub InvariantCulture (z lub bez IgnoreCase). Z drugiej strony, porównanie porządkowe błędnie uzna je za nierówne.

Niestety switchnie robi nic poza porównaniem porządkowym. Porównanie porządkowe jest dobre w przypadku niektórych rodzajów aplikacji, takich jak analiza pliku ASCII ze sztywno zdefiniowanymi kodami, ale w większości innych zastosowań porównanie ciągów porządkowych jest niewłaściwe.

To, co zrobiłem w przeszłości, aby uzyskać prawidłowe zachowanie, to po prostu makieta własnej instrukcji przełącznika. Można to zrobić na wiele sposobów. Jednym ze sposobów byłoby utworzenie List<T>par ciągów znaków i delegatów. Listę można przeszukiwać za pomocą odpowiedniego porównania ciągów. Po znalezieniu dopasowania można wywołać skojarzonego delegata.

Inną opcją jest wykonanie oczywistego łańcucha ifinstrukcji. Zwykle okazuje się, że nie jest tak źle, jak się wydaje, ponieważ struktura jest bardzo regularna.

Wspaniałą rzeczą w tym jest to, że tak naprawdę nie ma żadnego spadku wydajności w tworzeniu własnych funkcji przełącznika podczas porównywania z ciągami. System nie utworzy tablicy skoków O (1) w taki sposób, w jaki może to zrobić z liczbami całkowitymi, więc i tak będzie porównywał każdy łańcuch pojedynczo.

Jeśli istnieje wiele przypadków do porównania, a wydajność jest problemem, wówczas List<T>opisaną powyżej opcję można zastąpić posortowanym słownikiem lub tabelą skrótów. Wtedy wydajność może potencjalnie odpowiadać lub przekraczać opcję instrukcji switch.

Oto przykład listy delegatów:

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();
}

Oczywiście prawdopodobnie będziesz chciał dodać kilka standardowych parametrów i prawdopodobnie typ zwracany do delegata CustomSwitchDestination. I będziesz chciał tworzyć lepsze nazwy!

Jeśli zachowanie każdego z twoich przypadków nie pozwala na delegowanie wywołania w ten sposób, na przykład jeśli potrzebne są różne parametry, to utkniesz z połączonymi ifinstrukcjami. Zrobiłem to również kilka razy.

    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";
    }
Jeffrey L Whitledge
źródło
6
O ile się nie mylę, te dwa są różne tylko dla niektórych kultur (takich jak turecka), a w takim przypadku nie mógł użyć ToUpperInvariant()lub ToLowerInvariant()? Poza tym nie porównuje dwóch nieznanych ciągów , porównuje jeden nieznany ciąg z jednym znanym. Tak więc, o ile wie, jak zakodować na stałe odpowiednią reprezentację wielkich lub małych liter, blok przełącznika powinien działać dobrze.
Seth Petry-Johnson,
8
@Seth Petry-Johnson - Być może ta optymalizacja mogłaby zostać dokonana, ale powodem, dla którego opcje porównywania ciągów są wbudowane w framework, jest to, że nie wszyscy musimy być ekspertami lingwistycznymi, aby pisać poprawne, rozszerzalne oprogramowanie.
Jeffrey L Whitledge,
54
OK. Podam przykład, w którym jest to istotne. Załóżmy, że zamiast „house” mamy (angielskie!) Słowo „kawiarnia”. Ta wartość może być równie dobrze (i równie prawdopodobna) reprezentowana przez „caf \ u00E9” lub „cafe \ u0301”. Porządkowa równość (jak w instrukcji switch) z albo ToLower()albo ToLowerInvariant()zwróci wartość false. Equalsz StringComparison.InvariantCultureIgnoreCasezwróci prawdę. Ponieważ obie sekwencje wyglądają identycznie po wyświetleniu, ToLower()wersja jest paskudnym błędem do wyśledzenia. Dlatego zawsze najlepiej jest przeprowadzać właściwe porównania ciągów, nawet jeśli nie jesteś Turkiem.
Jeffrey L Whitledge,
77

Prostszym podejściem jest po prostu zmniejszenie wielkości liter przed przejściem do instrukcji switch i zmniejszenie wielkości liter.

Właściwie cholewka jest nieco lepsza z czysto ekstremalnego punktu widzenia nanosekund, ale mniej naturalnie na nią patrzeć.

Na przykład:

string s = "house"; 
switch (s.ToLower()) { 
  case "house": 
    s = "window"; 
    break;
}
Nick Craver
źródło
1
Tak, rozumiem, że małe litery to sposób, ale chcę, aby to było ignorowane. Czy istnieje sposób, w jaki mogę zastąpić instrukcję switch-case?
Tolsan,
6
@Lazarus - To jest z CLR przez C #, zostało opublikowane tutaj jakiś czas temu w wątku ukrytych funkcji: stackoverflow.com/questions/9033/hidden-features-of-c/ ... Możesz odpalić LinqPad z kilkoma milion iteracji, prawda.
Nick Craver
1
@Tolsan - Nie, niestety nie tylko ze względu na jego statyczny charakter. Jakiś czas temu pojawiła się na ten temat dobra porcja odpowiedzi: stackoverflow.com/questions/44905/…
Nick Craver
9
Wydaje się ToUpper(Invariant)to nie tylko szybsze, ale bardziej wiarygodne: stackoverflow.com/a/2801521/67824
Ohad Schneider
47

Przepraszamy za ten nowy wpis dotyczący starego pytania, ale jest nowa opcja rozwiązania tego problemu za pomocą C # 7 (VS 2017).

C # 7 oferuje teraz „dopasowywanie wzorców” i może być użyte do rozwiązania tego problemu w ten sposób:

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;
}

To rozwiązanie rozwiązuje również problem wspomniany w odpowiedzi @Jeffrey L Whitledge'a, że ​​porównanie ciągów bez uwzględniania wielkości liter to nie to samo, co porównanie dwóch napisów z małymi literami.

Nawiasem mówiąc, w lutym 2017 w Visual Studio Magazine pojawił się interesujący artykuł opisujący dopasowywanie wzorców i sposób ich wykorzystania w blokach wielkości liter. Proszę spojrzeć: Dopasowywanie wzorców w blokach wielkości liter C # 7.0

EDYTOWAĆ

W świetle odpowiedzi @ LewisM ważne jest, aby podkreślić, że switchstwierdzenie ma nowe, interesujące zachowanie. Oznacza to, że jeśli twoja caseinstrukcja zawiera deklarację zmiennej, to wartość określona wswitch części jest kopiowana do zmiennej zadeklarowanej w case. W poniższym przykładzie wartość truejest kopiowana do zmiennej lokalnej b. Ponadto zmienna bjest nieużywana i istnieje tylko po to, aby whenklauzula do caseinstrukcji mogła istnieć:

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

Jak wskazuje @LewisM, można to wykorzystać na korzyść - ta korzyść polega na tym, że porównywana rzecz jest faktycznie w switchzestawieniu, tak jak ma to miejsce w przypadku klasycznego użycia switchoświadczenia. Ponadto tymczasowe wartości zadeklarowane wcase instrukcji mogą zapobiec niechcianym lub niezamierzonym zmianom pierwotnej wartości:

switch(houseName)
{
    case string hn when hn.Equals("X", StringComparison.InvariantCultureIgnoreCase):
        windowName = "X-Window";
        break;
}
STLDev
źródło
2
Byłoby dłużej, ale wolałbym switch (houseName)wtedy zrobić porównanie podobnie jak Ty to zrobiłeś, czylicase var name when name.Equals("MyHouse", ...
LewisM
@LewisM - To interesujące. Czy możesz pokazać działający przykład?
STLDev
@LewisM - świetna odpowiedź. Dodałem dalszą dyskusję na temat przypisywania switchwartości argumentów do casezmiennych tymczasowych.
STLDev
Yay za dopasowywanie wzorców w nowoczesnym C #
Thiago Silva
Możesz również użyć „dopasowania wzorca obiektu” w ten case { } whensposób, aby nie martwić się o typ i nazwę zmiennej.
Bob
32

W niektórych przypadkach dobrym pomysłem może być użycie wyliczenia. Więc najpierw przeanalizuj wyliczenie (z flagą ignoreCase true), a następnie włącz przełącznik wyliczenia.

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
źródło
Tylko uwaga: Enum TryParse wydaje się być dostępne z Framework 4.0 i nowszymi wersjami FYI. msdn.microsoft.com/en-us/library/dd991317(v=vs.100).aspx
granadaCoder
4
Wolę to rozwiązanie, ponieważ zniechęca do używania magicznych strun.
user1069816
21

Rozszerzenie odpowiedzi autorstwa @STLDeveloperA. Nowy sposób obliczania instrukcji bez wielu instrukcji if od C # 7 polega na użyciu instrukcji Switch pasującej do wzorca, podobnie do sposobu, w jaki @STLDeveloper, chociaż w ten sposób włącza przełączaną zmienną

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;
}

Magazyn Visual Studio zawiera fajny artykuł na temat bloków obudów dopasowanych do wzorów, który może być wart obejrzenia.

LewisM
źródło
Dziękujemy za wskazanie dodatkowej funkcjonalności nowego switchzestawienia.
STLDev
5
+1 - to powinna być akceptowana odpowiedź dla nowoczesnego programowania (od C # 7). Jedną ze zmian, które bym wprowadził, jest kodowanie w ten sposób: case var name when "Bungalow".Equals(name, StringComparison.InvariantCultureIgnoreCase):ponieważ może to zapobiec zerowemu wyjątkowi odniesienia (gdzie houseName ma wartość null) lub alternatywnie dodać najpierw przypadek, w którym łańcuch ma wartość null.
Jay
19

Jednym z możliwych sposobów byłoby użycie słownika ignorowania wielkości liter z delegatem akcji.

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

dic["HouSe"]();

// Zwróć uwagę, że wywołanie nie zwraca tekstu, a jedynie wypełnia lokalną zmienną s.
// Jeśli chcesz zwrócić rzeczywisty tekst, zamień Actionna Func<string>i wartości w słowniku na coś podobnego() => "window2"

Magnus
źródło
4
Zamiast CurrentCultureIgnoreCase, OrdinalIgnoreCasejest korzystne.
Richard Ev
2
@richardEverett Preferred? Zależy od tego, czego chcesz, jeśli chcesz, aby aktualna kultura ignorowała wielkość liter, nie jest to preferowane.
Magnus
Jeśli ktoś jest zainteresowany, moje rozwiązanie (poniżej) przyjmuje ten pomysł i umieszcza go w prostej klasie.
Flydog57
2

Oto rozwiązanie, które zawija rozwiązanie @Magnus w klasę:

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();
    }
}

Oto przykład użycia go w prostej aplikacji Windows Form:

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

Jeśli używasz lambd (jak w przykładzie), otrzymujesz domknięcia, które przechwytują twoje zmienne lokalne (bardzo zbliżone do wrażenia, jakie daje instrukcja switch).

Ponieważ używa Dictionary pod okładkami, uzyskuje zachowanie O (1) i nie polega na przechodzeniu przez listę ciągów. Oczywiście musisz zbudować ten słownik, a to prawdopodobnie kosztuje więcej.

Prawdopodobnie miałoby sens dodanie prostej bool ContainsCase(string aCase)metody, która po prostu wywołuje ContainsKeymetodę słownika .

Flydog57
źródło
1

Mam nadzieję, że pomoże to w próbie przekonwertowania całego ciągu na określone litery, zarówno z małych, jak i dużych liter i użyj ciągu małych liter do porównania:

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
źródło
0

Powinno to wystarczyć:

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

Porównanie przełączników jest zatem niezmienne kulturowo. O ile widzę, powinno to dać taki sam wynik jak rozwiązania C # 7 z dopasowywaniem wzorców, ale bardziej zwięźle.

Kevin Bennett
źródło