Como fazer a instrução C # Switch usar IgnoreCase

92

Se eu tiver uma instrução switch-case em que o objeto na opção é string, é possível fazer uma comparação ignoreCase?

Eu tenho por exemplo:

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

Obterá so valor "janela"? Como faço para substituir a instrução switch-case para comparar as strings usando ignoreCase?

Tolsan
fonte

Respostas:

64

Como você parece estar ciente, colocar duas strings em minúsculas e compará-las não é o mesmo que fazer uma comparação de maiúsculas e minúsculas. Existem muitas razões para isso. Por exemplo, o padrão Unicode permite que o texto com diacríticos seja codificado de várias maneiras. Alguns caracteres incluem o caractere base e o diacrítico em um único ponto de código. Esses caracteres também podem ser representados como o caractere base seguido por um caractere diacrítico de combinação. Essas duas representações são iguais para todos os fins, e as comparações de strings de reconhecimento de cultura no .NET Framework irão identificá-los corretamente como iguais, com CurrentCulture ou InvariantCulture (com ou sem IgnoreCase). Uma comparação ordinal, por outro lado, os considerará incorretamente como desiguais.

Infelizmente, switchnão faz nada além de uma comparação ordinal. Uma comparação ordinal é adequada para certos tipos de aplicativos, como analisar um arquivo ASCII com códigos rigidamente definidos, mas a comparação de strings ordinais é incorreta para a maioria dos outros usos.

O que fiz no passado para obter o comportamento correto é apenas simular minha própria instrução switch. Existem muitas maneiras de fazer isso. Uma maneira seria criar um List<T>par de strings case e delegados. A lista pode ser pesquisada usando a comparação de string adequada. Quando a correspondência é encontrada, o delegado associado pode ser chamado.

Outra opção é fazer a cadeia óbvia de ifafirmações. Geralmente, isso não é tão ruim quanto parece, já que a estrutura é muito regular.

A melhor coisa sobre isso é que não há realmente nenhuma penalidade de desempenho em simular a funcionalidade de seu próprio switch ao comparar com strings. O sistema não vai fazer uma tabela de pulos O (1) da maneira que faz com inteiros, então vai comparar cada string uma por vez de qualquer maneira.

Se houver muitos casos a serem comparados e o desempenho for um problema, a List<T>opção descrita acima pode ser substituída por um dicionário classificado ou tabela de hash. Então, o desempenho pode corresponder ou exceder a opção de instrução switch.

Aqui está um exemplo da lista de delegados:

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

Claro, você provavelmente desejará adicionar alguns parâmetros padrão e, possivelmente, um tipo de retorno ao delegado CustomSwitchDestination. E você vai querer fazer nomes melhores!

Se o comportamento de cada um dos seus casos não puder ser delegado à invocação dessa maneira, como se parâmetros diferentes forem necessários, você estará preso a ifinstruções encadeadas . Eu também fiz isso algumas vezes.

    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
fonte
6
A menos que eu esteja enganado, os dois são diferentes apenas para certas culturas (como a turca) e, nesse caso, ele não poderia usar ToUpperInvariant()ou ToLowerInvariant()? Além disso, ele não está comparando duas strings desconhecidas , ele está comparando uma string desconhecida a uma string conhecida. Portanto, contanto que ele saiba como codificar a representação adequada em maiúsculas ou minúsculas, o bloco de comutação deve funcionar bem.
Seth Petry-Johnson
8
@Seth Petry-Johnson - Talvez essa otimização pudesse ser feita, mas o motivo pelo qual as opções de comparação de strings são incorporadas ao framework é que não precisamos nos tornar especialistas em linguística para escrever software extensível correto.
Jeffrey L Whitledge
59
ESTÁ BEM. Vou dar um exemplo em que isso é relevante. Suponha que em vez de "casa" tenhamos a palavra (em inglês) "café". Este valor pode ser representado igualmente bem (e igualmente provável) por "caf \ u00E9" ou "cafe \ u0301". A igualdade ordinal (como em uma instrução switch) com ToLower()ou ToLowerInvariant()retornará false. Equalscom StringComparison.InvariantCultureIgnoreCaseretornará verdadeiro. Como as duas sequências parecem idênticas quando exibidas, a ToLower()versão é um bug desagradável de rastrear. É por isso que é sempre melhor fazer comparações de strings adequadas, mesmo se você não for turco.
Jeffrey L Whitledge
78

Uma abordagem mais simples é apenas colocar sua string em minúsculas antes que ela vá para a instrução switch e ter as maiúsculas e minúsculas.

Na verdade, a parte superior é um pouco melhor do ponto de vista de desempenho de nanossegundos extremos, mas menos natural de se olhar.

Por exemplo:

string s = "house"; 
switch (s.ToLower()) { 
  case "house": 
    s = "window"; 
    break;
}
Nick Craver
fonte
1
Sim, eu entendo que minúsculas é uma forma, mas quero que ignoreCase. Existe uma maneira de substituir a instrução switch-case?
Tolsan
6
@Lazarus - Este é do CLR via C #, foi postado aqui há um tempo atrás no tópico de recursos ocultos também: stackoverflow.com/questions/9033/hidden-features-of-c/… Você pode iniciar o LinqPad com alguns milhões de iterações, é verdade.
Nick Craver
1
@Tolsan - Não, infelizmente não é só por causa de sua natureza estática. Houve um bom lote de respostas sobre isso há algum tempo: stackoverflow.com/questions/44905/…
Nick Craver
9
Parece que ToUpper(Invariant)não é apenas mais rápido, mas também mais confiável: stackoverflow.com/a/2801521/67824
Ohad Schneider
48

Desculpe por este novo post para uma pergunta antiga, mas há uma nova opção para resolver esse problema usando C # 7 (VS 2017).

C # 7 agora oferece "correspondência de padrões" e pode ser usado para resolver esse problema da seguinte maneira:

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

Esta solução também lida com o problema mencionado na resposta de @Jeffrey L Whitledge de que a comparação de strings sem distinção entre maiúsculas e minúsculas não é o mesmo que comparar duas strings com caixa baixa.

A propósito, houve um artigo interessante em fevereiro de 2017 na Visual Studio Magazine descrevendo a correspondência de padrões e como ela pode ser usada em blocos de caso. Por favor, dê uma olhada: Correspondência de padrões em blocos de caso C # 7.0

EDITAR

À luz da resposta de @LewisM, é importante ressaltar que a switchdeclaração tem um comportamento novo e interessante. Ou seja, se sua caseinstrução contiver uma declaração de variável, o valor especificado na switchparte será copiado para a variável declarada em case. No exemplo a seguir, o valor trueé copiado para a variável local b. Além disso, a variável bnão é utilizada e existe apenas para que a whencláusula da caseinstrução possa existir:

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

Como @LewisM aponta, isso pode ser usado para beneficiar - esse benefício é que a coisa que está sendo comparada está na verdade na switchinstrução, como acontece com o uso clássico da switchinstrução. Além disso, os valores temporários declarados na caseinstrução podem evitar alterações indesejadas ou inadvertidas no valor original:

switch(houseName)
{
    case string hn when hn.Equals("X", StringComparison.InvariantCultureIgnoreCase):
        windowName = "X-Window";
        break;
}
STLDev
fonte
2
Seria mais longo, mas eu preferiria switch (houseName)fazer a comparação semelhante à maneira como você fez, ou sejacase var name when name.Equals("MyHouse", ...
LewisM
@LewisM - Isso é interessante. Você pode mostrar um exemplo prático disso?
STLDev
@LewisM - ótima resposta. Eu adicionei mais discussão sobre a atribuição de switchvalores de argumento a casevariáveis ​​temporárias.
STLDev
Oba para correspondência de padrões em C # moderno
Thiago Silva
Você também pode usar "correspondência de padrão de objeto", case { } whenassim não precisa se preocupar com o tipo e o nome da variável.
Bob,
33

Em alguns casos, pode ser uma boa ideia usar um enum. Portanto, primeiro analise o enum (com o sinalizador ignoreCase true) e, em seguida, tenha uma opção no 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;
   }
}
uli78
fonte
Apenas uma nota: Enum TryParse parece estar disponível com Framework 4.0 e posterior, FYI. msdn.microsoft.com/en-us/library/dd991317(v=vs.100).aspx
granadaCoder
4
Prefiro essa solução porque desencoraja o uso de cordas mágicas.
user1069816
23

Uma extensão da resposta de @STLDeveloperA. Uma nova maneira de fazer a avaliação de instruções sem várias instruções if a partir de c # 7 é usar a instrução Switch de correspondência de padrões, semelhante à maneira como @STLDeveloper, embora desta forma, ligue a variável que está sendo trocada

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

A revista visual studio tem um bom artigo sobre blocos de caixas de correspondência de padrões que pode valer a pena dar uma olhada.

LewisM
fonte
Obrigado por apontar a funcionalidade adicional da nova switchdeclaração.
STLDev
5
+1 - esta deve ser a resposta aceita para o desenvolvimento moderno (C # 7 em diante). Uma alteração que eu faria é codificar assim: case var name when "Bungalow".Equals(name, StringComparison.InvariantCultureIgnoreCase):pois isso pode evitar uma exceção de referência nula (em que houseName é nula) ou, alternativamente, adicionar um caso para a string ser nula primeiro.
Jay
21

Uma maneira possível seria usar um dicionário de caso para ignorar com um delegado de ação.

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

dic["HouSe"]();

// Observe que a chamada não retorna texto, mas apenas preenche a variável local s.
// Se você deseja retornar o texto real, substitua os valores Actionpara Func<string>e no dicionário por algo como() => "window2"

Magnus
fonte
4
Em vez de CurrentCultureIgnoreCase, OrdinalIgnoreCaseé o preferido.
Richard Ev
2
@richardEverett Preferred? Depende do que você deseja, se você deseja que a cultura atual ignore maiúsculas e minúsculas, não é preferível.
Magnus
Se alguém estiver interessado, minha solução (abaixo) pega essa ideia e a envolve em uma aula simples.
Flydog57
2

Aqui está uma solução que envolve a solução de @Magnus em uma 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();
    }
}

Aqui está um exemplo de como usá-lo em um aplicativo simples do Windows Form:

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

Se você usar lambdas (como o exemplo), obterá fechamentos que capturarão suas variáveis ​​locais (muito parecido com a sensação que você obtém de uma instrução switch).

Como ele usa um Dicionário nos bastidores, ele obtém comportamento O (1) e não depende de percorrer a lista de strings. Claro, você precisa construir esse dicionário, e isso provavelmente custa mais.

Provavelmente faria sentido adicionar um bool ContainsCase(string aCase)método simples que simplesmente chamasse o ContainsKeymétodo do dicionário .

Flydog57
fonte
1

Espero que isso ajude a tentar converter toda a string em letras minúsculas ou maiúsculas e usar a string em letras minúsculas para comparação:

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
fonte
0

Deve ser suficiente fazer isso:

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

A comparação do switch é, portanto, invariante de cultura. Pelo que posso ver, isso deve atingir o mesmo resultado que as soluções de correspondência de padrões C # 7, mas de forma mais sucinta.

Kevin Bennett
fonte
-1

10 anos depois, com a correspondência de padrões C #, você pode fazer algo como:

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