Cambiar declaración fallthrough en C #?

365

Fallthrough sentencia switch es una de mis principales razones personales para amar switchvs. if/else ifconstrucciones. Un ejemplo está en orden aquí:

static string NumberToWords(int number)
{
    string[] numbers = new string[] 
        { "", "one", "two", "three", "four", "five", 
          "six", "seven", "eight", "nine" };
    string[] tens = new string[] 
        { "", "", "twenty", "thirty", "forty", "fifty", 
          "sixty", "seventy", "eighty", "ninety" };
    string[] teens = new string[]
        { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
          "sixteen", "seventeen", "eighteen", "nineteen" };

    string ans = "";
    switch (number.ToString().Length)
    {
        case 3:
            ans += string.Format("{0} hundred and ", numbers[number / 100]);
        case 2:
            int t = (number / 10) % 10;
            if (t == 1)
            {
                ans += teens[number % 10];
                break;
            }
            else if (t > 1)
                ans += string.Format("{0}-", tens[t]);
        case 1:
            int o = number % 10;
            ans += numbers[o];

            break;
        default:
            throw new ArgumentException("number");
    }
    return ans;
}

Las personas inteligentes se encogen porque las string[]s deben declararse fuera de la función: bueno, lo son, este es solo un ejemplo.

El compilador falla con el siguiente error:

El control no puede pasar de una etiqueta de caso ('caso 3:') a otro
El control no puede pasar de una etiqueta de caso ('caso 2:') a otro

¿Por qué? ¿Y hay alguna forma de obtener este tipo de comportamiento sin tener tres ifs?

Matthew Scharley
fuente

Respuestas:

648

(Copiar / pegar una respuesta que proporcioné en otro lugar )

La caída de switch- cases se puede lograr sin tener un código en a case(ver case 0), o usando los formularios especiales goto case(ver case 1) o goto default(ver case 2):

switch (/*...*/) {
    case 0: // shares the exact same code as case 1
    case 1:
        // do something
        goto case 2;
    case 2:
        // do something else
        goto default;
    default:
        // do something entirely different
        break;
}
Alex Lyman
fuente
121
Creo que, en este caso particular, goto no se considera dañino.
Thomas Owens
78
Maldición: he estado programando con C # desde los primeros días de 1.0, y nunca había visto esto hasta ahora. Solo va a mostrar, aprendes cosas nuevas todos los días.
Erik Forbes
37
Todo está bien, Erik. La única razón por la que lo sabía es que soy un nerd de la teoría de compiladores que leí las especificaciones ECMA-334 con una lupa.
Alex Lyman
13
@Dancrumb: en el momento en que se escribió la función, C # aún no había agregado ninguna palabra clave "blanda" (como 'rendimiento', 'var', 'de' y 'seleccionar'), por lo que tenían tres opciones reales: 1 ) convierta 'fallthrough' en una palabra clave difícil (no puede usarla como nombre de variable), 2) escriba el código necesario para admitir una palabra clave tan flexible, 3) use palabras clave ya reservadas. El n. ° 1 fue un gran problema para los que portaban código; # 2 fue una tarea de ingeniería bastante grande, por lo que entiendo; y la opción con la que se fueron, # 3 tuvo un beneficio secundario: otros desarrolladores que leen el código después de que el hecho pueda aprender sobre la característica del concepto base de goto
Alex Lyman
10
Todo esto habla de palabras clave nuevas / especiales para una caída explícita. ¿No podrían simplemente haber usado la palabra clave 'continuar'? En otras palabras. Salga del interruptor o continúe con el siguiente caso (caída).
Tal Even-Tov
44

El "por qué" es evitar la caída accidental, por lo que estoy agradecido. Esta es una fuente no común de errores en C y Java.

La solución alternativa es usar goto, p. Ej.

switch (number.ToString().Length)
{
    case 3:
        ans += string.Format("{0} hundred and ", numbers[number / 100]);
        goto case 2;
    case 2:
    // Etc
}

El diseño general de switch / case es un poco desafortunado en mi opinión. Se quedó demasiado cerca de C: hay algunos cambios útiles que podrían hacerse en términos de alcance, etc. Podría decirse que un interruptor más inteligente que podría hacer coincidir patrones, etc. sería útil, pero eso realmente está cambiando de interruptor a "verificar una secuencia de condiciones" - en ese punto quizás se requeriría un nombre diferente.

Jon Skeet
fuente
1
Esta es la diferencia entre switch y if / elseif en mi mente. El interruptor es para verificar varios estados de una sola variable, mientras que if / elseif puede usarse para verificar cualquier cantidad de cosas que se conectan, pero no necesariamente una sola variable o la misma variable.
Matthew Scharley
2
Si fuera para evitar una caída accidental, creo que una advertencia del compilador hubiera sido mejor. Al igual que tiene uno si su declaración if tiene una asignación:if (result = true) { }
Tal Even-Tov
3
@ TalEven-Tov: las advertencias del compilador realmente deberían ser para casos en los que casi siempre se puede corregir el código para que sea mejor. Personalmente, preferiría la ruptura implícita, por lo que no sería un problema para empezar, pero ese es un asunto diferente.
Jon Skeet
26

Switch Fallthrough es históricamente una de las principales fuentes de errores en los softwares modernos. El diseñador de idiomas decidió hacer obligatorio el salto al final del caso, a menos que esté omitiendo el siguiente caso directamente sin procesamiento.

switch(value)
{
    case 1:// this is still legal
    case 2:
}
Coincoin
fuente
23
Nunca entiendo por qué ese no es "caso 1, 2:"
BCS
11
@David Pfeffer: Sí, y también case 1, 2:en los idiomas que lo permiten. Lo que nunca entenderé es por qué cualquier lenguaje moderno no elegiría permitir eso.
BCS
@BCS con la instrucción goto, ¿podrían ser difíciles de manejar múltiples opciones separadas por comas?
pingüino
@pengut: podría ser más exacto decir que case 1, 2:es una etiqueta única pero con varios nombres. - FWIW, creo que la mayoría de los idiomas que prohíben la caída no hacen caso especial a las "etiquetas de caso consecutivas", sino que tratan las etiquetas de caso como una anotación en la siguiente declaración y requieren la última declaración antes de una declaración etiquetada con (uno o más) etiquetas de casos para ser un salto.
BCS
22

Para agregar a las respuestas aquí, creo que vale la pena considerar la pregunta opuesta junto con esto, a saber. ¿Por qué C permitió la caída en primer lugar?

Cualquier lenguaje de programación, por supuesto, tiene dos objetivos:

  1. Proporcionar instrucciones a la computadora.
  2. Deje un registro de las intenciones del programador.

La creación de cualquier lenguaje de programación es, por lo tanto, un equilibrio entre la mejor manera de servir estos dos objetivos. Por un lado, cuanto más fácil sea convertir las instrucciones de la computadora (ya sean códigos de máquina, bytecode como IL, o las instrucciones se interpretan en la ejecución), más capaz será ese proceso de compilación o interpretación para ser eficiente, confiable y compacto en salida. Llevado a su extremo, este objetivo da como resultado que solo escribamos en ensamblador, IL o incluso códigos de operación sin procesar, porque la compilación más fácil es donde no hay compilación en absoluto.

Por el contrario, cuanto más expresa el lenguaje la intención del programador, en lugar de los medios utilizados para ese fin, más comprensible es el programa tanto al escribir como durante el mantenimiento.

Ahora, switch siempre podría haberse compilado convirtiéndolo en la cadena de if-elsebloques equivalente o similar, pero fue diseñado para permitir la compilación en un patrón de ensamblaje común particular donde uno toma un valor, calcula un desplazamiento de él (ya sea buscando una tabla indexado por un hash perfecto del valor, o por aritmética real en el valor *). Vale la pena señalar en este punto que hoy, la compilación de C # a veces se convertirá switchen el equivalenteif-else , y a veces utilizará un enfoque de salto basado en hash (y de la misma manera con C, C ++ y otros lenguajes con sintaxis comparable).

En este caso, hay dos buenas razones para permitir la caída:

  1. De todos modos, sucede de forma natural: si construye una tabla de salto en un conjunto de instrucciones, y uno de los lotes de instrucciones anteriores no contiene algún tipo de salto o retorno, la ejecución progresará naturalmente al siguiente lote. Permitir la caída era lo que "sucedería" si giraba elswitch C en una tabla de salto, utilizando el código de máquina.

  2. Los codificadores que escribieron en conjunto ya estaban acostumbrados al equivalente: al escribir una tabla de salto a mano en conjunto, tendrían que considerar si un bloque de código dado terminaría con un retorno, un salto fuera de la tabla, o simplemente continuaría a la siguiente cuadra. Como tal, hacer que el codificador agregue un explícito breakcuando sea necesario también fue "natural" para el codificador.

En ese momento, por lo tanto, fue un intento razonable de equilibrar los dos objetivos de un lenguaje de computadora en lo que se refiere tanto al código de máquina producido como a la expresividad del código fuente.

Sin embargo, cuatro décadas después, las cosas no son exactamente iguales, por algunas razones:

  1. Los codificadores en C hoy pueden tener poca o ninguna experiencia en ensamblaje. Los codificadores en muchos otros lenguajes de estilo C son aún menos propensos (¡especialmente Javascript!). Cualquier concepto de "a lo que las personas están acostumbradas desde la asamblea" ya no es relevante.
  2. Las mejoras en las optimizaciones significan que la probabilidad de que switchcualquiera se convierta enif-else porque se consideró que el enfoque es más eficiente, o que se convierta en una variante particularmente esotérica del enfoque de la tabla de salto es mayor. El mapeo entre los enfoques de nivel superior e inferior no es tan fuerte como lo era antes.
  3. La experiencia ha demostrado que la caída tiende a ser el caso minoritario más que la norma (un estudio del compilador de Sun encontró que el 3% de los switchbloques usaron una caída distinta de las etiquetas múltiples en el mismo bloque, y se pensó que el uso- caso aquí significaba que este 3% era, de hecho, mucho más alto de lo normal). Entonces, el lenguaje estudiado hace que lo inusual se atienda más fácilmente que lo común.
  4. La experiencia ha demostrado que la falla tiende a ser la fuente de problemas, tanto en los casos en que se realiza accidentalmente, como también en los casos en que alguien que mantiene el código pasa por alto la falla correcta. Esta última es una adición sutil a los errores asociados con la falla, porque incluso si su código está perfectamente libre de fallas, su falla puede causar problemas.

En relación con esos dos últimos puntos, considere la siguiente cita de la edición actual de K&R:

Caer de un caso a otro no es robusto, siendo propenso a la desintegración cuando se modifica el programa. Con la excepción de múltiples etiquetas para un solo cálculo, los fallos deben usarse con moderación y comentarse.

Como una buena forma, ponga un descanso después del último caso (el predeterminado aquí) aunque sea lógicamente innecesario. Algún día, cuando se agregue otro caso al final, este poco de programación defensiva lo salvará.

Entonces, desde la boca del caballo, la caída en C es problemática. Se considera una buena práctica documentar siempre las fallas con comentarios, lo cual es una aplicación del principio general de que uno debe documentar dónde se hace algo inusual, porque eso es lo que hará que el examen posterior del código y / o haga que su código se vea así tiene un error de novato cuando de hecho es correcto.

Y cuando lo piensas, codifica así:

switch(x)
{
  case 1:
   foo();
   /* FALLTHRU */
  case 2:
    bar();
    break;
}

Es agregando algo para hacer que la caída sea explícita en el código, simplemente no es algo que el compilador pueda detectar (o cuya ausencia se pueda detectar).

Como tal, el hecho de que on tiene que ser explícito con fall-through en C # no agrega ninguna penalización a las personas que escribieron bien en otros lenguajes de estilo C de todos modos, ya que ya serían explícitos en sus fallos. †

Finalmente, el uso de gotohere ya es una norma de C y otros lenguajes similares:

switch(x)
{
  case 0:
  case 1:
  case 2:
    foo();
    goto below_six;
  case 3:
    bar();
    goto below_six;
  case 4:
    baz();
    /* FALLTHRU */
  case 5:
  below_six:
    qux();
    break;
  default:
    quux();
}

En este tipo de caso en el que queremos que se incluya un bloque en el código ejecutado para un valor distinto de aquel que trae uno al bloque anterior, entonces ya tenemos que usarlo goto. (Por supuesto, hay medios y formas de evitar esto con diferentes condicionales, pero eso es cierto en casi todo lo relacionado con esta pregunta). Como tal, C # se basó en la forma ya normal de lidiar con una situación en la que queremos golpear más de un bloque de código en un switch, y simplemente lo generalizó para cubrir también la falla. También hizo que ambos casos fueran más convenientes y autodocumentados, ya que tenemos que agregar una nueva etiqueta en C pero podemos usarla casecomo etiqueta en C #. En C # podemos deshacernos de la below_sixetiqueta y usarlagoto case 5 que es más claro en cuanto a lo que estamos haciendo. (También tendríamos que agregarbreakpara el default, que dejé fuera solo para hacer que el código C anterior claramente no sea el código C #).

En resumen, por lo tanto:

  1. C # ya no se relaciona con la salida no optimizada del compilador tan directamente como lo hizo el código C hace 40 años (ni tampoco lo hace C en estos días), lo que hace que una de las inspiraciones de la caída sea irrelevante.
  2. C # sigue siendo compatible con C no solo por tener implícito break, para que los que estén familiarizados con lenguajes más sencillos aprendan el idioma y porten más fácilmente.
  3. C # elimina una posible fuente de errores o código mal entendido que ha sido bien documentado como causante de problemas durante las últimas cuatro décadas.
  4. C # hace que el compilador ejecute las mejores prácticas existentes con C (caída del documento).
  5. C # hace que el caso inusual sea el que tiene un código más explícito, el caso habitual es el que tiene el código que solo se escribe automáticamente.
  6. C # usa el mismo gotoenfoque basado en la base para golpear el mismo bloque desde diferentes caseetiquetas como se usa en C. Simplemente lo generaliza a otros casos.
  7. C # hace que ese gotoenfoque basado en la base sea más conveniente y más claro que en C, al permitir que las casedeclaraciones actúen como etiquetas.

En definitiva, una decisión de diseño bastante razonable


* Algunas formas de BASIC le permitirían a uno hacer cosas similares, GOTO (x AND 7) * 50 + 240mientras que frágil y, por lo tanto, un caso particularmente persuasivo para la prohibición goto, sirve para mostrar un equivalente en un idioma superior del tipo de forma en que el código de nivel inferior puede hacer un salto basado en aritmética sobre un valor, que es mucho más razonable cuando es el resultado de la compilación en lugar de algo que debe mantenerse manualmente. Las implementaciones del dispositivo de Duff en particular se prestan bien al código de máquina equivalente o IL porque cada bloque de instrucciones a menudo tendrá la misma longitud sin necesidad de agregar noprellenos.

† El dispositivo de Duff vuelve a aparecer aquí, como una excepción razonable. El hecho de que con eso y patrones similares haya una repetición de operaciones sirve para hacer que el uso de la caída sea relativamente claro, incluso sin un comentario explícito al respecto.

Jon Hanna
fuente
17

Puede 'ir a la etiqueta del caso' http://www.blackwasp.co.uk/CSharpGoto.aspx

La instrucción goto es un comando simple que transfiere incondicionalmente el control del programa a otra instrucción. El comando a menudo se critica con algunos desarrolladores que recomiendan su eliminación de todos los lenguajes de programación de alto nivel porque puede conducir a un código de espagueti . Esto ocurre cuando hay tantas declaraciones goto o declaraciones de salto similares que el código se vuelve difícil de leer y mantener. Sin embargo, hay programadores que señalan que la declaración goto, cuando se usa con cuidado, proporciona una solución elegante a algunos problemas ...

kenny
fuente
8

Dejaron de lado este comportamiento por diseño para evitar cuando no fue utilizado por voluntad pero causó problemas.

Se puede usar solo si no hay una declaración en la parte del caso, como:

switch (whatever)
{
    case 1:
    case 2:
    case 3: boo; break;
}
Biri
fuente
4

Cambiaron el comportamiento de la declaración de cambio (de C / Java / C ++) para c #. Supongo que el razonamiento fue que la gente se olvidó de la caída y se causaron errores. Un libro que leí decía que usaba goto para simular, pero esto no me parece una buena solución.

Conocer
fuente
2
C # soporta goto, pero no fallthrough? Guau. Y no son solo esos. C # es el único lenguaje que conozco que se comporta de esta manera.
Matthew Scharley
Al principio no me gustó exactamente, pero "fall-thru" es realmente una receta para el desastre (especialmente entre los programadores junior). Como muchos han señalado, C # todavía permite fall-thru para líneas vacías (que es la mayoría de casos.) "Kenny" publicó un enlace que destaca el uso elegante de Goto con el interruptor de caja.
Pretzel
No es un gran problema en mi opinión. El 99% de las veces no quiero caer y he sido quemado por errores en el pasado.
Ken
1
"Esto no me parece una buena solución", lamento escuchar eso de ti, porque para eso goto caseestá. Su ventaja sobre el fracaso es que es explícito. El hecho de que algunas personas se opongan goto casesimplemente demuestra que fueron adoctrinados contra el "goto" sin comprender el problema y que son incapaces de pensar por sí mismos. Cuando Dijkstra escribió "GOTO Considerado Dañino", se refería a idiomas que no tenían ningún otro medio para cambiar el flujo de control.
Jim Balter
@JimBalter y luego, ¿cuántas personas que citan a Dijkstra en eso citarán a Knuth que "la optimización prematura es la raíz de todo mal" aunque esa cita fue cuando Knuth estaba escribiendo explícitamente sobre cuán útil gotopuede ser al optimizar el código?
Jon Hanna
1

Puede lograr caer como C ++ por la palabra clave goto.

EX:

switch(num)
{
   case 1:
      goto case 3;
   case 2:
      goto case 3;
   case 3:
      //do something
      break;
   case 4:
      //do something else
      break;
   case default:
      break;
}
Dai Tran
fuente
99
¡Ojalá alguien hubiera publicado eso dos años antes!
0

Se requiere una declaración de salto como un descanso después de cada bloque de casos, incluido el último bloque, ya sea una declaración de caso o una declaración predeterminada. Con una excepción, (a diferencia de la declaración de cambio de C ++), C # no admite una caída implícita de una etiqueta de caso a otra. La única excepción es si una declaración de caso no tiene código.

- Documentación de C # switch ()

Powerlord
fuente
1
Me doy cuenta de que este comportamiento está documentado, quiero saber POR QUÉ es así y las alternativas para obtener el comportamiento anterior.
Matthew Scharley
0

Después de cada declaración de caso, se requiere una declaración de interrupción o goto , incluso si es un caso predeterminado.

Shilpa gulati
fuente
2
¡Ojalá alguien hubiera publicado eso dos años antes!
1
@Poldie fue divertido la primera vez ... Shilpa, no necesitas un descanso o ir a cada caso, solo para cada caso con su propio código. Puede tener múltiples casos que comparten código bien.
Maverick
0

Solo una nota rápida para agregar que el compilador de Xamarin realmente se equivocó y permite fallos. Supuestamente se ha solucionado, pero no se ha publicado. Descubrí esto en algún código que en realidad estaba fallando y el compilador no se quejó.


fuente
0

el interruptor (Referencia de C #) dice

C # requiere el final de las secciones del interruptor, incluida la final,

Por lo tanto, también debe agregar un break;a su defaultsección, de lo contrario seguirá habiendo un error del compilador.

gm2008
fuente
Gracias, esto me ayudó;)
CareTaker22
-1

C # no admite fallos con declaraciones de cambio / caso. No estoy seguro de por qué, pero realmente no hay soporte para ello. Enlace

BFree
fuente
Esto esta muy mal. Ver las otras respuestas ... el efecto de una caída implícita se puede obtener con un explícito goto case . Esta fue una elección de diseño intencional y sabia por parte de los diseñadores de C #.
Jim Balter
-11

Olvidó agregar el "descanso"; declaración en el caso 3. En el caso 2 lo escribió en el bloque if. Por lo tanto, intente esto:

case 3:            
{
    ans += string.Format("{0} hundred and ", numbers[number / 100]);
    break;
}


case 2:            
{
    int t = (number / 10) % 10;            
    if (t == 1)            
    {                
        ans += teens[number % 10];                
    }            
    else if (t > 1)                
    {
        ans += string.Format("{0}-", tens[t]);        
    }
    break;
}

case 1:            
{
    int o = number % 10;            
    ans += numbers[o];            
    break;        
}

default:            
{
    throw new ArgumentException("number");
}
Marcus
fuente
2
Esto produce un resultado muy incorrecto. Dejé las declaraciones de cambio por diseño. La pregunta es por qué el compilador de C # ve esto como un error cuando casi ningún otro idioma tiene esta restricción.
Matthew Scharley
55
Qué asombroso fracaso para comprender. ¿Y ha tenido 5 años para eliminar esto y aún no lo ha hecho? Alucinante.
Jim Balter