Come fare in modo che l'istruzione C # Switch utilizzi IgnoreCase


92

Se ho un'istruzione switch-case in cui l'oggetto nell'interruttore è una stringa, è possibile eseguire un confronto ignoreCase?

Ho ad esempio:

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

Otterrà sil valore "finestra"? Come faccio a sovrascrivere l'istruzione switch-case in modo da confrontare le stringhe utilizzando ignoreCase?

Risposte:


64

Come sembra che tu sappia, mettere in minuscolo due stringhe e confrontarle non è la stessa cosa che fare un confronto tra maiuscole e minuscole. Ci sono molte ragioni per questo. Ad esempio, lo standard Unicode consente di codificare il testo con segni diacritici in più modi. Alcuni caratteri includono sia il carattere di base che il segno diacritico in un unico punto di codice. Questi caratteri possono anche essere rappresentati come il carattere di base seguito da un carattere diacritico combinato. Queste due rappresentazioni sono uguali a tutti gli effetti e i confronti di stringhe con riconoscimento delle impostazioni cultura in .NET Framework le identificheranno correttamente come uguali, con CurrentCulture o InvariantCulture (con o senza IgnoreCase). Un confronto ordinale, d'altra parte, li considererà erroneamente disuguali.

Sfortunatamente, switchnon fa altro che un confronto ordinale. Un confronto ordinale va bene per alcuni tipi di applicazioni, come l'analisi di un file ASCII con codici definiti in modo rigido, ma il confronto di stringhe ordinali è sbagliato per la maggior parte degli altri usi.

Quello che ho fatto in passato per ottenere il comportamento corretto è solo una simulazione della mia dichiarazione di switch. Ci sono molti modi per farlo. Un modo sarebbe creare una List<T>coppia di stringhe case e delegati. L'elenco può essere ricercato utilizzando il confronto di stringhe appropriato. Quando viene trovata la corrispondenza, è possibile richiamare il delegato associato.

Un'altra opzione è fare l'ovvia catena di ifaffermazioni. Questo di solito risulta non essere così grave come sembra, poiché la struttura è molto regolare.

La cosa grandiosa di questo è che non c'è davvero alcuna penalità nelle prestazioni nel deridere la propria funzionalità switch quando si confronta con le stringhe. Il sistema non creerà una tabella di salto O (1) come può con i numeri interi, quindi confronterà comunque ogni stringa una alla volta.

Se ci sono molti casi da confrontare e le prestazioni sono un problema, l' List<T>opzione descritta sopra potrebbe essere sostituita con un dizionario ordinato o una tabella hash. Quindi le prestazioni possono potenzialmente corrispondere o superare l'opzione dell'istruzione switch.

Ecco un esempio dell'elenco dei delegati:

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

Ovviamente, probabilmente vorrai aggiungere alcuni parametri standard e possibilmente un tipo restituito al delegato CustomSwitchDestination. E vorrai fare nomi migliori!

Se il comportamento di ciascuno dei tuoi casi non è suscettibile di delegare l'invocazione in questo modo, ad esempio se sono necessari parametri diversi, allora sei bloccato con ifdichiarazioni concatenate . L'ho fatto anche alcune volte.

    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
A meno che non mi sbagli, i due sono diversi solo per certe culture (come il turco), e in quel caso non poteva usare ToUpperInvariant()o ToLowerInvariant()? Inoltre, non sta confrontando due stringhe sconosciute , sta confrontando una stringa sconosciuta con una stringa nota. Pertanto, fintanto che sa come codificare la rappresentazione appropriata in maiuscolo o minuscolo, il blocco interruttore dovrebbe funzionare bene.
Seth Petry-Johnson,

8
@Seth Petry-Johnson - Forse questa ottimizzazione potrebbe essere fatta, ma il motivo per cui le opzioni di confronto delle stringhe sono integrate nel framework è che non dobbiamo diventare tutti esperti di linguistica per scrivere software corretto ed estensibile.
Jeffrey L Whitledge,

59
OK. Faccio un esempio in cui questo è rilevante. Supponiamo che al posto di "casa" avessimo la parola (inglese!) "Café". Questo valore potrebbe essere rappresentato altrettanto bene (e altrettanto probabile) da "caf \ u00E9" o "cafe \ u0301". L'uguaglianza ordinale (come in un'istruzione switch) con ToLower()o ToLowerInvariant()restituirà false. Equalswith StringComparison.InvariantCultureIgnoreCaserestituirà true. Poiché entrambe le sequenze sembrano identiche quando vengono visualizzate, la ToLower()versione è un brutto bug da rintracciare. Questo è il motivo per cui è sempre meglio fare confronti di stringhe corretti, anche se non sei turco.
Jeffrey L Whitledge,

78

Un approccio più semplice consiste semplicemente nel mettere in minuscolo la stringa prima che entri nell'istruzione switch e avere i casi più bassi.

In realtà, la tomaia è un po 'meglio dal punto di vista delle prestazioni estreme in nanosecondi, ma meno naturale da guardare.

Per esempio:

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

1
Sì, capisco che il minuscolo è un modo, ma voglio che venga ignoreCase. C'è un modo per sovrascrivere l'istruzione switch-case?
Tolsan

6
@Lazarus - Questo è da CLR tramite C #, è stato pubblicato qui qualche tempo fa anche nel thread delle funzionalità nascoste: stackoverflow.com/questions/9033/hidden-features-of-c/… Puoi attivare LinqPad con alcuni milioni di iterazioni, è vero.
Nick Craver

1
@Tolsan - No, purtroppo non c'è solo per la sua natura statica. C'era una buona serie di risposte su questo un po 'di tempo fa: stackoverflow.com/questions/44905/…
Nick Craver

9
Sembra che ToUpper(Invariant)non sia solo più veloce, ma anche più affidabile: stackoverflow.com/a/2801521/67824
Ohad Schneider


48

Ci scusiamo per questo nuovo post su una vecchia domanda, ma c'è una nuova opzione per risolvere questo problema usando C # 7 (VS 2017).

C # 7 ora offre il "pattern matching" e può essere utilizzato per risolvere questo problema in questo modo:

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

Questa soluzione affronta anche il problema menzionato nella risposta di @Jeffrey L Whitledge secondo cui il confronto delle stringhe senza distinzione tra maiuscole e minuscole non è la stessa cosa del confronto di due stringhe in minuscolo.

A proposito, nel febbraio 2017 è stato pubblicato un articolo interessante su Visual Studio Magazine che descriveva la corrispondenza dei modelli e come può essere utilizzato nei blocchi del caso. Dai un'occhiata: Pattern Matching in C # 7.0 Case Blocks

MODIFICARE

Alla luce della risposta di @ LewisM, è importante sottolineare che l' switchaffermazione ha un comportamento nuovo e interessante. Vale a dire che se la tua caseistruzione contiene una dichiarazione di variabile, il valore specificato nella switchparte viene copiato nella variabile dichiarata in case. Nell'esempio seguente, il valore trueviene copiato nella variabile locale b. Oltre a ciò, la variabile bè inutilizzata ed esiste solo in modo che possa esistere la whenclausola casedell'istruzione:

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

Come sottolinea @LewisM, questo può essere utilizzato per trarre vantaggio - il vantaggio è che la cosa confrontata è effettivamente nell'affermazione switch, come è con l'uso classico switchdell'affermazione. Inoltre, i valori temporanei dichiarati casenell'istruzione possono impedire modifiche indesiderate o involontarie al valore originale:

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

2
Sarebbe più lungo, ma preferirei switch (houseName)fare il confronto in modo simile al modo in cui lo hai fatto tu, ovverocase var name when name.Equals("MyHouse", ...
LewisM

@ LewisM - Interessante. Puoi mostrare un esempio funzionante di questo?
STLDev

@ LewisM - ottima risposta. Ho aggiunto ulteriori discussioni sull'assegnazione dei switchvalori degli argomenti alle casevariabili temporanee.
STLDev

Yay for pattern matching in modern C #
Thiago Silva

Puoi anche usare la "corrispondenza del modello di oggetto" in questo modo, case { } whencosì non devi preoccuparti del tipo e del nome della variabile.
Bob

33

In alcuni casi potrebbe essere una buona idea usare un'enumerazione. Quindi prima analizza l'enum (con il flag ignoreCase true) e poi hai un'opzione sull'enum.

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

Solo una nota: Enum TryParse sembra essere disponibile con Framework 4.0 e versioni successive, FYI. msdn.microsoft.com/en-us/library/dd991317(v=vs.100).aspx
granadaCoder

4
Preferisco questa soluzione in quanto scoraggia l'uso di corde magiche.
user1069816

23

Un'estensione alla risposta di @STLDeveloperA. Un nuovo modo per eseguire la valutazione dell'istruzione senza più istruzioni if ​​a partire da c # 7 sta usando l'istruzione Switch di corrispondenza del modello, simile al modo in cui @STLDeveloper sebbene in questo modo accenda la variabile che viene cambiata

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

La rivista dello studio visivo ha un bell'articolo sui blocchi di casi di corrispondenza dei modelli che potrebbe valere la pena dare un'occhiata.


Grazie per aver sottolineato le funzionalità aggiuntive della nuova switchdichiarazione.
STLDev

5
+1: questa dovrebbe essere la risposta accettata per lo sviluppo moderno (da C # 7 in poi). Una modifica che vorrei apportare è che vorrei codificare in questo modo: case var name when "Bungalow".Equals(name, StringComparison.InvariantCultureIgnoreCase):poiché ciò può impedire un'eccezione di riferimento null (dove houseName è null), o in alternativa aggiungere un caso in cui la stringa sia null prima.
Jay

21

Un modo possibile sarebbe utilizzare un dizionario di maiuscole e minuscole con un delegato di azione.

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

dic["HouSe"]();

// Notare che la chiamata non restituisce testo, ma popola solo le variabili locali s.
// Se si desidera restituire il testo effettivo, sostituire Actionai Func<string>valori e nel dizionario per una cosa del genere() => "window2"


4
Piuttosto che CurrentCultureIgnoreCase, OrdinalIgnoreCaseè preferito.
Richard Ev,

2
@richardEverett Preferred? Dipende da cosa vuoi, se vuoi che la cultura corrente ignori il caso non è preferito.
Magnus

Se qualcuno è interessato, la mia soluzione (sotto) prende questa idea e la racchiude in una semplice classe.
Flydog57

2

Ecco una soluzione che racchiude la soluzione di @Magnus in una classe:

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

Ecco un esempio di come utilizzarlo in una semplice app di Windows Form:

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

Se usi lambda (come nell'esempio), ottieni chiusure che cattureranno le tue variabili locali (abbastanza vicino alla sensazione che ottieni da un'istruzione switch).

Poiché utilizza un dizionario sotto le coperte, ottiene il comportamento O (1) e non si basa sull'esplorazione dell'elenco di stringhe. Certo, devi costruire quel dizionario e probabilmente costa di più.

Probabilmente avrebbe senso aggiungere un bool ContainsCase(string aCase)metodo semplice che chiama semplicemente il ContainsKeymetodo del dizionario .


1

Spero che questo aiuti a provare a convertire l'intera stringa in lettere minuscole o maiuscole e utilizzare la stringa minuscola per il confronto:

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

0

Dovrebbe essere sufficiente per fare questo:

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

Il confronto degli interruttori è quindi invariante dalla cultura. Per quanto posso vedere, questo dovrebbe ottenere lo stesso risultato delle soluzioni C # 7 Pattern-Matching, ma in modo più succinto.


-1

10 anni dopo, con la corrispondenza del modello C #, puoi fare qualcosa come:

private string NormalisePropertyType(string propertyType) => true switch
{
    true when string.IsNullOrWhiteSpace(propertyType) => propertyType,
    true when "house".Equals(propertyType, StringComparison.OrdinalIgnoreCase) => "house",
    true when "window".Equals(propertyType, StringComparison.OrdinalIgnoreCase) => "window",
    true when "door".Equals(propertyType, StringComparison.OrdinalIgnoreCase) => "door",
    true when "roof".Equals(propertyType, StringComparison.OrdinalIgnoreCase) => "roof",
    true when "chair".Equals(propertyType, StringComparison.OrdinalIgnoreCase) => "chair",
    _ => propertyType
};
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.