Cómo hacer que C # Switch Statement use IgnoreCase

89

Si tengo una declaración de caso de cambio donde el objeto en el cambio es una cadena, ¿es posible hacer una comparación ignoreCase?

Tengo por ejemplo:

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

¿ sObtendrá el valor "ventana"? ¿Cómo anulo la instrucción switch-case para que compare las cadenas usando ignoreCase?

Tolsan
fuente

Respuestas:

63

Como parece saber, poner dos cadenas en minúsculas y compararlas no es lo mismo que hacer una comparación de caso omiso. Hay muchas razones para esto. Por ejemplo, el estándar Unicode permite codificar texto con signos diacríticos de varias formas. Algunos caracteres incluyen tanto el carácter base como el diacrítico en un solo punto de código. Estos caracteres también pueden representarse como el carácter base seguido de un carácter diacrítico combinado. Estas dos representaciones son iguales para todos los propósitos, y las comparaciones de cadenas con reconocimiento cultural en .NET Framework las identificarán correctamente como iguales, ya sea con CurrentCulture o InvariantCulture (con o sin IgnoreCase). Una comparación ordinal, por otro lado, los considerará incorrectamente como desiguales.

Desafortunadamente, switchno hace nada más que una comparación ordinal. Una comparación ordinal está bien para ciertos tipos de aplicaciones, como analizar un archivo ASCII con códigos rígidamente definidos, pero la comparación de cadenas ordinales es incorrecta para la mayoría de los otros usos.

Lo que he hecho en el pasado para obtener el comportamiento correcto es simplemente simular mi propia declaración de cambio. Hay muchas formas de hacer esto. Una forma sería crear un List<T>par de cadenas de casos y delegados. Se puede buscar en la lista utilizando la comparación de cadenas adecuada. Cuando se encuentra la coincidencia, se puede invocar al delegado asociado.

Otra opción es hacer la cadena obvia de ifdeclaraciones. Esto generalmente resulta no ser tan malo como parece, ya que la estructura es muy regular.

Lo bueno de esto es que realmente no hay ninguna penalización en el rendimiento al simular la funcionalidad de su propio interruptor cuando se compara con cadenas. El sistema no va a hacer una tabla de salto O (1) como puede hacerlo con números enteros, por lo que comparará cada cadena una a la vez de todos modos.

Si hay muchos casos para comparar y el rendimiento es un problema, entonces la List<T>opción descrita anteriormente podría reemplazarse con un diccionario ordenado o una tabla hash. Entonces, el rendimiento puede igualar o superar potencialmente la opción de declaración de cambio.

A continuación se muestra un ejemplo de la 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();
}

Por supuesto, probablemente querrá agregar algunos parámetros estándar y posiblemente un tipo de retorno al delegado CustomSwitchDestination. ¡Y querrás hacer mejores nombres!

Si el comportamiento de cada uno de sus casos no es susceptible de delegar la invocación de esta manera, por ejemplo, si son necesarios diferentes parámetros, entonces está atascado con ifdeclaraciones encadenadas . También he hecho esto algunas veces.

    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
fuente
6
A menos que me equivoque, los dos solo son diferentes para ciertas culturas (como la turca), y en ese caso, ¿no podría usar ToUpperInvariant()o ToLowerInvariant()? Además, no está comparando dos cadenas desconocidas , está comparando una cadena desconocida con una cadena conocida. Por lo tanto, siempre que sepa cómo codificar la representación adecuada en mayúsculas o minúsculas, el bloque de interruptores debería funcionar bien.
Seth Petry-Johnson
8
@Seth Petry-Johnson: tal vez se podría hacer esa optimización, pero la razón por la que las opciones de comparación de cadenas están integradas en el marco es para que no todos tengamos que convertirnos en expertos en lingüística para escribir software correcto y extensible.
Jeffrey L Whitledge
54
OKAY. Daré un ejemplo donde esto es relevante. Supongamos que en lugar de "casa" tuviéramos la palabra (¡en inglés!) "Café". Este valor podría representarse igualmente bien (y con la misma probabilidad) por "caf \ u00E9" o "café \ u0301". La igualdad ordinal (como en una declaración de cambio) con ToLower()o ToLowerInvariant()devolverá falso. Equalscon StringComparison.InvariantCultureIgnoreCasevolverá verdadero. Dado que ambas secuencias se ven idénticas cuando se muestran, la ToLower()versión es un error desagradable de rastrear. Es por eso que siempre es mejor hacer comparaciones de cadenas adecuadas, incluso si no eres turco.
Jeffrey L Whitledge
77

Un enfoque más simple es simplemente poner en minúsculas su cadena antes de que entre en la declaración de cambio, y hacer que los casos sean más bajos.

En realidad, la parte superior es un poco mejor desde el punto de vista del rendimiento de nanosegundos extremos, pero menos natural a la vista.

P.ej:

string s = "house"; 
switch (s.ToLower()) { 
  case "house": 
    s = "window"; 
    break;
}
Nick Craver
fuente
1
Sí, entiendo que usar minúsculas es una forma, pero quiero que sea ignoreCase. ¿Hay alguna manera de que pueda anular la declaración de cambio de caso?
Tolsan
6
@Lazarus: esto es de CLR a través de C #, también se publicó aquí hace un tiempo en el hilo de características ocultas: stackoverflow.com/questions/9033/hidden-features-of-c/… Puede iniciar LinqPad con algunos millones de iteraciones, es cierto.
Nick Craver
1
@Tolsan - No, desafortunadamente no existe solo por su naturaleza estática. Hubo un buen lote de respuestas sobre esto hace un tiempo: stackoverflow.com/questions/44905/…
Nick Craver
9
Parece ToUpper(Invariant)que no solo es más rápido, sino más confiable: stackoverflow.com/a/2801521/67824
Ohad Schneider
47

Lo siento por esta nueva publicación a una pregunta anterior, pero hay una nueva opción para resolver este problema usando C # 7 (VS 2017).

C # 7 ahora ofrece "coincidencia de patrones" y se puede utilizar para abordar este problema de la siguiente manera:

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 solución también se ocupa del problema mencionado en la respuesta de @Jeffrey L Whitledge de que la comparación de cadenas sin distinción entre mayúsculas y minúsculas no es lo mismo que comparar dos cadenas en minúsculas.

Por cierto, hubo un artículo interesante en febrero de 2017 en Visual Studio Magazine que describe la coincidencia de patrones y cómo se puede usar en bloques de casos. Por favor, eche un vistazo: Coincidencia de patrones en bloques de casos de C # 7.0

EDITAR

A la luz de la respuesta de @ LewisM, es importante señalar que la switchdeclaración tiene un comportamiento nuevo e interesante. Es decir, si su casedeclaración contiene una declaración de variable, entonces el valor especificado en la switchparte se copia en la variable declarada en case. En el siguiente ejemplo, el valor truese copia en la variable local b. Además de eso, la variable bno se usa y existe solo para que la whencláusula de la casedeclaración pueda existir:

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

Como señala @LewisM, esto se puede usar para beneficiarse; ese beneficio es que lo que se está comparando está realmente en la switchdeclaración, como lo está con el uso clásico de la switchdeclaración. Además, los valores temporales declarados en la casedeclaración pueden evitar cambios no deseados o inadvertidos en el valor original:

switch(houseName)
{
    case string hn when hn.Equals("X", StringComparison.InvariantCultureIgnoreCase):
        windowName = "X-Window";
        break;
}
STLDev
fuente
2
Sería más largo, pero preferiría switch (houseName)hacer la comparación similar a la forma en que lo hizo, es decircase var name when name.Equals("MyHouse", ...
LewisM
@LewisM - Eso es interesante. ¿Puede mostrar un ejemplo práctico de eso?
STLDev
@LewisM - gran respuesta. Agregué más discusión sobre la asignación de switchvalores de argumentos a casevariables temporales.
STLDev
Hurra por la coincidencia de patrones en C # moderno
Thiago Silva
También puede utilizar la "coincidencia de patrones de objetos" para case { } whenno tener que preocuparse por el tipo y el nombre de la variable.
Bob
32

En algunos casos, puede ser una buena idea usar una enumeración. Entonces, primero analice la enumeración (con la bandera ignoreCase verdadera) y luego active la enumeración.

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
fuente
Solo una nota: Enum TryParse parece estar disponible con Framework 4.0 y versiones posteriores, FYI. msdn.microsoft.com/en-us/library/dd991317(v=vs.100).aspx
granadaCoder
4
Prefiero esta solución ya que desalienta el uso de cuerdas mágicas.
user1069816
21

Una extensión de la respuesta de @STLDeveloperA. Una nueva forma de hacer la evaluación de declaraciones sin múltiples declaraciones if a partir de c # 7 es usar la declaración Switch de coincidencia de patrones, similar a la forma en que @STLDeveloper, aunque de esta manera está activando la variable que se está cambiando

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 revista Visual Studio tiene un buen artículo sobre bloques de casos de coincidencia de patrones que podría valer la pena ver.

LewisM
fuente
Gracias por señalar la funcionalidad adicional de la nueva switchdeclaración.
STLDev
5
+1: esta debería ser la respuesta aceptada para el desarrollo moderno (C # 7 en adelante). Un cambio que haría es que codificaría así: case var name when "Bungalow".Equals(name, StringComparison.InvariantCultureIgnoreCase):ya que esto puede evitar una excepción de referencia nula (donde houseName es nulo) o, alternativamente, agregar un caso para que la cadena sea nula primero.
Jay
19

Una forma posible sería utilizar un diccionario de casos ignorados con un delegado de acción.

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

dic["HouSe"]();

// Tenga en cuenta que la llamada no devuelve texto, solo llena las variables locales s.
// Si desea devolver el texto real, reemplace Actiona Func<string>y valores en el diccionario a algo así como() => "window2"

Magnus
fuente
4
En lugar de CurrentCultureIgnoreCase, OrdinalIgnoreCasese prefiere.
Richard Ev
2
@richardEverett ¿Preferido? Depende de lo que desee, si desea que la cultura actual ignore el caso, no es el preferido.
Magnus
Si alguien está interesado, mi solución (a continuación) toma esta idea y la resume en una clase simple.
Flydog57
2

Aquí hay una solución que envuelve la solución de @Magnus en una clase:

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

Aquí hay un ejemplo de cómo usarlo en una aplicación simple de Windows Form:

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

Si usa lambdas (como el ejemplo), obtendrá cierres que capturarán sus variables locales (bastante cerca de la sensación que obtiene de una declaración de cambio).

Dado que utiliza un diccionario oculto, obtiene un comportamiento O (1) y no se basa en recorrer la lista de cadenas. Por supuesto, necesita construir ese diccionario, y eso probablemente cueste más.

Probablemente tenga sentido agregar un bool ContainsCase(string aCase)método simple que simplemente llame al ContainsKeymétodo del diccionario .

Flydog57
fuente
1

Espero que esto ayude a tratar de convertir toda la cadena en minúsculas o mayúsculas y usar la cadena en minúsculas para comparar:

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

Debería ser suficiente para hacer esto:

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

Por tanto, la comparación de conmutadores no varía según la cultura. Por lo que puedo ver, esto debería lograr el mismo resultado que las soluciones C # 7 Pattern-Matching, pero de manera más sucinta.

Kevin Bennett
fuente