¿Tiene sentido crear bloques solo para reducir el alcance de una variable?


38

Estoy escribiendo un programa en Java donde en un momento necesito cargar una contraseña para mi almacén de claves. Solo por diversión, intenté mantener mi contraseña en Java lo más breve posible haciendo esto:

//Some code
....

KeyManagerFactory keyManager = KeyManagerFactory.getInstance("SunX509");
Keystore keyStore = KeyStore.getInstance("JKS");

{
    char[] password = getPassword();
    keyStore.load(new FileInputStream(keyStoreLocation), password);
    keyManager.init(keyStore, password);
}

...
//Some more code

Ahora, sé que en este caso es un poco tonto. Hay muchas otras cosas que podría haber hecho, la mayoría de ellas realmente mejores (no podría haber usado una variable en absoluto).

Sin embargo, tenía curiosidad si había un caso en el que hacer esto no fuera tan tonto. Lo único que se me ocurre es si desea reutilizar nombres de variables comunes como count, o temp, pero las convenciones de nomenclatura y los métodos cortos hacen que sea poco probable que sea útil.

¿Hay algún caso en el que usar bloques solo para reducir el alcance variable tenga sentido?


13
@gnat ... ¿qué? Ni siquiera sé cómo editar mi pregunta para que sea menos similar a ese objetivo de engaño. ¿Qué parte de esto te llevó a creer que estas preguntas son las mismas?
Lord Farquaad

22
La respuesta habitual a esto es que su bloque anónimo debe ser un método con un nombre significativo en su lugar, que le proporciona documentación automática y el alcance reducido que desea. Sin pensarlo, no puedo pensar en una situación en la que extraer un método no sea mejor que insertar un bloque desnudo.
Kilian Foth

44
Hay un par de preguntas similares que hacen esto para C ++: ¿Es una mala práctica crear bloques de código? y Código de estructura con llaves . Quizás uno de esos contenga una respuesta a esta pregunta, aunque C ++ y Java son muy diferentes a este respecto.
amon


44
@gnat ¿Por qué esa pregunta está abierta? Es una de las cosas más amplias que he visto. ¿Es un intento de una pregunta canónica?
jpmc26

Respuestas:


34

Primero, hablando de la mecánica subyacente:

En C ++ ámbito == de por vida se invocan destructores b / c a la salida del ámbito. Además, una distinción importante en C / C ++ podemos declarar objetos locales. En el tiempo de ejecución para C / C ++, el compilador generalmente asignará un marco de pila para el método que sea tan grande como sea necesario, por adelantado, en lugar de asignar más espacio de pila en la entrada a cada ámbito (que declara variables). Entonces, el compilador está colapsando o aplanando los ámbitos.

El compilador de C / C ++ puede reutilizar el espacio de almacenamiento de la pila para locales que no entren en conflicto con la vida útil (generalmente usará el análisis del código real para determinar esto en lugar del alcance, ¡b / c que es más preciso que el alcance!).

Menciono la sintaxis de Java C / C ++ b / c, es decir, llaves y alcance, al menos en parte derivada de esa familia. Y también porque C ++ surgió en los comentarios de las preguntas.

Por el contrario, no es posible tener objetos locales en Java: todos los objetos son objetos de montón, y todos los locales / formales son variables de referencia o tipos primitivos (por cierto, lo mismo es cierto para las estadísticas).

Además, en Java, el alcance y la vida útil no se equiparan exactamente: estar dentro / fuera del alcance es principalmente un concepto de tiempo de compilación que va a accesibilidad y conflictos de nombres; nada sucede realmente en Java al salir del alcance con respecto a la limpieza de variables. La recolección de basura de Java determina (el punto final de la) vida útil de los objetos.

Además, el mecanismo de código de bytes de Java (la salida del compilador de Java) tiende a promover las variables de usuario declaradas dentro de ámbitos limitados al nivel superior del método, porque no hay una función de alcance en el nivel de código de bytes (esto es similar al manejo C / C ++ del apilar). En el mejor de los casos, el compilador podría reutilizar ranuras de variables locales, pero (a diferencia de C / C ++) solo si su tipo es el mismo.

(Sin embargo, para asegurarse de que el compilador JIT subyacente en el tiempo de ejecución podría reutilizar el mismo espacio de almacenamiento de pila para dos locales localizados de forma diferente (y con ranuras diferentes), con un análisis suficiente del código de bytes).

Hablando en beneficio del programador, tendería a estar de acuerdo con otros en que un método (privado) es una mejor construcción, incluso si se usa solo una vez.

Aún así, no hay nada realmente malo en usarlo para desconfiar nombres.

Lo he hecho en raras circunstancias cuando hacer métodos individuales es una carga. Por ejemplo, al escribir un intérprete usando una switchdeclaración grande dentro de un ciclo, podría (dependiendo de los factores) introducir un bloque separado para cada uno casepara mantener los casos individuales más separados entre sí, en lugar de hacer que cada caso sea un método diferente (cada uno que solo se invoca una vez).

(Tenga en cuenta que, como código en bloques, los casos individuales tienen acceso a las declaraciones "break;" y "continue;" con respecto al ciclo de cierre, mientras que los métodos requerirían retornos booleanos y el uso de condicionales de la persona que llama para obtener acceso a estos flujos de control declaraciones)


Muy interesante respuesta (+1)! Sin embargo, un complemento a la parte de C ++. Si la contraseña es una cadena en un bloque, el objeto de cadena asignará espacio en algún lugar de la tienda libre para la parte de tamaño variable. Al salir del bloque, la cadena se destruirá, dando como resultado la desasignación de la parte variable. Pero como no está en la pila, nada garantiza que se sobrescribirá pronto. Así que gestione mejor la confidencialidad sobrescribiendo cada uno de sus caracteres antes de que se destruya la cadena ;-)
Christophe

1
@ErikEidt Intentaba llamar la atención sobre el problema de hablar de "C / C ++" como si realmente fuera una "cosa", pero no lo es: "En el tiempo de ejecución de C / C ++, el compilador generalmente asignará un marco de pila para el método que sea tan grande como sea necesario ", pero C no tiene métodos en absoluto. Tiene funciones. Estos son diferentes, especialmente en C ++.
tchrist

77
" Por el contrario, no es posible tener objetos locales en Java: todos los objetos son objetos de montón y todos los locales / formales son variables de referencia o tipos primitivos (por cierto, lo mismo es cierto para las estadísticas) ". Tenga en cuenta que esto es no del todo cierto, especialmente no para las variables locales del método. El análisis de escape puede asignar objetos a la pila.
Boris the Spider

1
En el mejor de los casos, el compilador podría reutilizar ranuras de variables locales, pero (a diferencia de C / C ++) solo si su tipo es el mismo. "Simplemente está mal. A nivel de bytecode, las variables locales pueden asignarse y reasignarse con lo que desee, la única restricción es que el uso posterior es compatible con el último tipo de elemento asignado. Reutilizando ranuras de variables locales es la norma, por ejemplo, con { int x = 42; String s="blah"; } { long y = 0; }, la longvariable será reutilizar las ranuras de las dos variables de, xy scon todos los compiladores utilizados comúnmente ( javacy ecj).
Holger

1
@ Boris the Spider: esa es una declaración coloquial repetida a menudo que realmente no coincide con lo que está sucediendo. El análisis de escape puede habilitar la escalarización, que es la descomposición de los campos del objeto en variables, que luego están sujetas a optimizaciones adicionales. Algunos de ellos pueden eliminarse como no utilizados, otros pueden plegarse con las variables de origen utilizadas para inicializarlos, otros pueden asignarse a los registros de la CPU. El resultado rara vez coincide con lo que la gente podría imaginar al decir "asignado en la pila".
Holger

27

En mi opinión, sería más claro extraer el bloque con su propio método. Si dejó este bloque, espero ver un comentario que aclare por qué está poniendo el bloque allí en primer lugar. En ese momento, se siente más limpio tener la llamada al método initializeKeyManager()que mantiene la variable de contraseña limitada al alcance del método y muestra su intención con el bloque de código.


2
Creo que su respuesta elude por un caso de uso clave, es un paso intermedio durante una refactorización. Puede ser útil limitar el alcance y saber que su código aún funciona justo antes de extraer un método.
RubberDuck

16
@RubberDuck es cierto, pero en ese caso el resto de nosotros nunca debería verlo, así que no importa lo que pensemos. Lo que un codificador adulto y un compilador consiente consigan en la privacidad de su propio cubículo no es asunto de nadie más.
candied_orange

@candied_orange Me temo que no lo entiendo :( ¿
Quieres

@doubleOrt Quise decir que los pasos intermedios de refactorización son reales y necesarios, pero ciertamente no necesitan ser consagrados en el control de la fuente donde todos puedan verlo. Simplemente termine de refactorizar antes de registrarse.
candied_orange

@candied_orange Oh, pensé que estabas discutiendo y no extendiendo el argumento de RubberDuck.
doubleOrt

17

Pueden ser útiles en Rust, con sus estrictas reglas de préstamo. Y, en general, en lenguajes funcionales, donde ese bloque devuelve un valor. ¿Pero en lenguajes como Java? Si bien la idea parece elegante, en la práctica rara vez se usa, por lo que la confusión al verla eclipsará la claridad potencial de limitar el alcance de las variables.

Sin embargo, hay un caso en el que encuentro esto útil: ¡en una switchdeclaración! A veces quieres declarar una variable en un case:

switch (op) {
    case ADD:
        int result = a + b;
        System.out.printf("%s + %s = %s\n", a, b, result);
        break;
    case SUB:
        int result = a - b;
        System.out.printf("%s - %s = %s\n", a, b, result);
        break;
}

Esto, sin embargo, fallará:

error: variable result is already defined in method main(String[])
            int result = a - b;

switch las declaraciones son extrañas: son declaraciones de control, pero debido a su caída no introducen ámbitos.

Entonces, puede usar bloques para crear los ámbitos usted mismo:

switch (op) {
    case ADD: {
        int result = a + b;
        System.out.printf("%s + %s = %s\n", a, b, result);
    } break;
    case SUB: {
        int result = a - b;
        System.out.printf("%s - %s = %s\n", a, b, result);
    } break;
}

6
  • Caso general:

¿Hay algún caso en el que usar bloques solo para reducir el alcance variable tenga sentido?

Generalmente se considera una buena práctica mantener los métodos cortos. Reducir el alcance de algunas variables puede ser una señal de que su método es bastante largo, lo suficiente como para que piense que el alcance de las variables debe reducirse. En este caso, probablemente valga la pena crear un método separado para esa parte del código.

Los casos que me parecerían problemáticos es cuando esa parte del código usa más que un puñado de argumentos. Hay situaciones en las que podría terminar con pequeños métodos que requieren una gran cantidad de parámetros (o necesitan una solución alternativa para simular la devolución de múltiples valores). La solución a ese problema en particular es crear otra clase que mantenga las diversas variables que usa e implemente implementa todos los pasos que tiene en mente en sus métodos relativamente cortos: este es un caso de uso típico para usar un patrón Builder .

Dicho esto, todas estas pautas de codificación se pueden aplicar libremente a veces. A veces hay casos extraños, donde el código puede ser más legible si lo mantiene en un método un poco más largo, en lugar de dividirlo en pequeños submétodos (o patrones de construcción). Por lo general, esto funciona para problemas que son de tamaño mediano, bastante lineales y donde demasiada división realmente dificulta la legibilidad (especialmente si necesita saltar entre demasiados métodos para comprender lo que hace el código).

Puede tener sentido, pero es muy raro.

  • Su caso de uso:

Aquí está tu código:

KeyManagerFactory keyManager = KeyManagerFactory.getInstance("SunX509");
Keystore keyStore = KeyStore.getInstance("JKS");

{
    char[] password = getPassword();
    keyStore.load(new FileInputStream(keyStoreLocation), password);
    keyManager.init(keyStore, password);
}

Estás creando un FileInputStream, pero nunca lo estás cerrando.

Una forma de resolver esto es usar una declaración de prueba con recursos . Alcanza el alcance InputStream, y también puede usar este bloque para reducir el alcance de otras variables si lo desea (dentro de lo razonable, ya que estas líneas están relacionadas):

KeyManagerFactory keyManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
Keystore keyStore = KeyStore.getInstance("JKS");

try (InputStream fis = new FileInputStream(keyStoreLocation)) {
    char[] password = getPassword();
    keyStore.load(fis, password);
    keyManager.init(keyStore, password);
}

(Por cierto, también es mejor usarlo en getDefaultAlgorithm()lugar de hacerlo SunX509).

Además, si bien el consejo de separar fragmentos de código en métodos separados es generalmente correcto. Raramente vale la pena si es solo por 3 líneas (si es que hay algo, ese es un caso en el que crear un método separado dificulta la legibilidad).


"Reducir el alcance de algunas variables puede ser una señal de que su método es bastante largo". Puede ser una señal, pero también se puede usar y en mi humilde opinión se debe utilizar para documentar cuánto tiempo se usa el valor de una variable local. De hecho, para los idiomas que no admiten ámbitos anidados (por ejemplo, Python), agradecería si un comentario de código correspondiente indicara el "fin de la vida útil" de una variable. Si la misma variable se reutiliza más adelante, sería fácil saber si debe (o debería) haberse inicializado con un nuevo valor en algún lugar antes (pero después del comentario de "fin de vida").
Jonny Dee

2

En mi humilde opinión, generalmente es algo que debe evitarse en gran medida para el código de producción porque generalmente no está tentado a hacerlo, excepto en funciones esporádicas que realizan tareas dispares. Tiendo a hacerlo en algún código de desecho utilizado para probar cosas, pero no encuentro la tentación de hacerlo en el código de producción, donde he pensado con anticipación qué debe hacer cada función, ya que, naturalmente, la función tendrá alcance limitado con respecto a su estado local.

Nunca he visto ejemplos de bloques anónimos que se usen de esta manera (sin involucrar condicionales, un trybloque para una transacción, etc.) para reducir el alcance de una manera significativa en una función que no plantea la pregunta de por qué no pudo se dividirá aún más en funciones más simples con alcances reducidos si realmente se benefició prácticamente desde un punto de vista de SE genuino desde los bloques anónimos. Por lo general, es un código ecléctico que está haciendo un montón de cosas poco relacionadas o muy poco relacionadas donde estamos más tentados a alcanzar esto.

Como ejemplo, si está tratando de hacer esto para reutilizar una variable llamada count, entonces sugiere que está contando dos cosas dispares. Si el nombre de la variable será tan corto como countpara mí, entonces tiene sentido vincularlo con el contexto de la función, que podría estar contando un tipo de cosa. Luego puede ver instantáneamente el nombre y / o la documentación de la función, ver county saber instantáneamente lo que significa en el contexto de lo que está haciendo la función sin analizar todo el código. A menudo no encuentro un buen argumento para que una función cuente dos cosas diferentes reutilizando el mismo nombre de variable de manera que los ámbitos / bloques anónimos sean tan atractivos en comparación con las alternativas. Eso no sugiere que todas las funciones tengan que contar solo una cosa. YO'beneficio de ingeniería para una función que reutiliza el mismo nombre de variable para contar dos o más cosas y usa bloques anónimos para limitar el alcance de cada recuento individual. Si la función es simple y clara, no es el fin del mundo tener dos variables de conteo con nombres diferentes, y la primera posiblemente tenga algunas líneas más de visibilidad de alcance de lo que idealmente se requiere. Dichas funciones generalmente no son la fuente de errores que carecen de tales bloques anónimos para reducir aún más el alcance mínimo de sus variables locales.

No es una sugerencia para métodos superfluos

Esto no es para sugerirle que cree métodos con fuerza, ya sea solo para reducir el alcance. Podría decirse que es igual de malo o peor, y lo que sugiero no debería exigir la necesidad de métodos "auxiliares" privados incómodos más que la necesidad de ámbitos anónimos. Eso es pensar demasiado en el código tal como está ahora y cómo reducir el alcance de las variables que pensar en cómo resolver conceptualmente el problema a nivel de la interfaz de manera que produzca una visibilidad limpia y corta de los estados de la función local de forma natural sin una anidación profunda de bloques y más de 6 niveles de sangría. Estoy de acuerdo con Bruno en que puede obstaculizar la legibilidad del código insertando con fuerza 3 líneas de código en una función, pero eso comienza con el supuesto de que está desarrollando las funciones que crea en función de su implementación existente, en lugar de diseñar las funciones sin enredarse en implementaciones. Si lo hace de esta manera, he encontrado poca necesidad de bloques anónimos que no sirvan para nada más allá de reducir el alcance de la variable dentro de un método dado, a menos que esté tratando celosamente de reducir el alcance de una variable con solo unas pocas líneas menos de código inofensivo donde la introducción exótica de estos bloques anónimos posiblemente contribuya con la sobrecarga intelectual que eliminan.

Tratando de reducir los alcances mínimos aún más

Si valía la pena reducir los ámbitos de variables locales al mínimo absoluto, entonces debería haber una amplia aceptación de código como este:

ImageIO.write(new Robot("borg").createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())), "png", new File(Db.getUserId(User.handle()).toString()));

... ya que eso causa la visibilidad mínima del estado al ni siquiera crear variables para referirse a ellas en primer lugar. No quiero parecer dogmático, pero en realidad creo que la solución pragmática es evitar los bloques anónimos cuando sea posible, así como evitar la monstruosa línea de código anterior, y si parecen tan absolutamente necesarios en un contexto de producción del perspectiva de la corrección y el mantenimiento de invariantes dentro de una función, entonces definitivamente creo que vale la pena volver a examinar cómo está organizando su código en funciones y diseñando sus interfaces. Naturalmente, si su método tiene 400 líneas de largo y el alcance de una variable es visible para 300 líneas de código más de lo necesario, eso podría ser un verdadero problema de ingeniería, pero no es necesariamente un problema para resolver con bloques anónimos.

Por lo menos, el uso de bloques anónimos en todo el lugar es exótico, no idiomático, y el código exótico conlleva el riesgo de ser odiado por otros, si no por usted mismo, años después.

La utilidad práctica de reducir el alcance

La utilidad final de reducir el alcance de las variables es permitirle obtener la administración de estado correcta y mantenerla correcta y permitirle razonar fácilmente sobre lo que hace cualquier parte de una base de código, para poder mantener invariantes conceptuales. Si la administración de estado local de una sola función es tan compleja que tiene que reducir a la fuerza el alcance con un bloque anónimo en el código que no está destinado a ser finalizado y bueno, nuevamente, eso es una señal para mí de que la función en sí misma necesita ser reexaminada . Si tiene dificultades para razonar sobre el manejo del estado de las variables en un ámbito de función local, imagine la dificultad de razonar sobre las variables privadas accesibles para cada método de una clase completa. No podemos usar bloques anónimos para reducir su visibilidad. Para mí, ayuda comenzar con la aceptación de que las variables tenderán a tener un alcance ligeramente más amplio de lo que idealmente necesitan tener en muchos idiomas, siempre que no se salga de control hasta el punto en que tenga dificultades para mantener invariantes. No es algo para resolver tanto con bloques anónimos como lo veo como aceptar desde un punto de vista pragmático.


Como advertencia final, como mencioné, todavía utilizo bloques anónimos de vez en cuando, a menudo en los principales métodos de punto de entrada para el código de desecho. Alguien más lo mencionó como una herramienta de refactorización y creo que está bien, ya que el resultado es intermediario y está destinado a ser refactorizado, no a un código finalizado. No estoy descartando su utilidad por completo, pero si está tratando de escribir código que sea ampliamente aceptado, probado y correcto, apenas veo la necesidad de bloques anónimos. Obsesionarse por usarlos puede alejar el enfoque del diseño de funciones más claras con bloques naturalmente pequeños.

2

¿Hay algún caso en el que usar bloques solo para reducir el alcance variable tenga sentido?

Creo que la motivación es razón suficiente para introducir un bloqueo, sí.

Como señaló Casey, el bloque es un "olor a código" que indica que es mejor que extraiga el bloque en una función. Pero no puedes.

John Carmark escribió una nota sobre el código en línea en 2007. Su resumen final

  • Si una función solo se llama desde un solo lugar, considere incluirla.
  • Si se llama a una función desde varios lugares, vea si es posible organizar el trabajo para que se realice en un solo lugar, tal vez con banderas, y en línea.
  • Si hay varias versiones de una función, considere hacer una sola función con más parámetros, posiblemente predeterminados.
  • Si el trabajo es casi puramente funcional, con pocas referencias al estado global, intente hacerlo completamente funcional.

Así que piense , haga una elección con un propósito y (opcional) deje evidencia que describa su motivación para la elección que hizo.


1

Mi opinión puede ser controvertida, pero sí, creo que ciertamente hay situaciones en las que usaría este tipo de estilo. Sin embargo, "solo para reducir el alcance de la variable" no es un argumento válido, porque eso es lo que ya está haciendo. Y hacer algo solo para hacerlo no es una razón válida. También tenga en cuenta que esta explicación no tiene sentido si su equipo ya ha resuelto si este tipo de sintaxis es convencional o no.

Principalmente soy un programador de C #, pero he oído que en Java, devolver una contraseña como char[]es para permitirle reducir el tiempo que la contraseña permanecerá en la memoria. En este ejemplo, no ayudará, ya que el recolector de basura puede recolectar la matriz desde el momento en que no está en uso, por lo que no importa si la contraseña abandona el alcance. No estoy discutiendo si es viable borrar la matriz después de que haya terminado con ella, pero en este caso, si desea hacerlo, el alcance de la variable tiene sentido:

{
    char[] password = getPassword();
    try{
        keyStore.load(new FileInputStream(keyStoreLocation), password);
        keyManager.init(keyStore, password);
    }finally{
        Arrays.fill(password, '\0');
    }
}

Esto es realmente similar a la declaración de prueba con recursos , porque examina el recurso y realiza una finalización después de que haya terminado. Una vez más, tenga en cuenta que no estoy discutiendo el manejo de las contraseñas de esta manera, solo que si decide hacerlo, este estilo es razonable.

La razón de esto es que la variable ya no es válida. Lo ha creado, usado e invalidado su estado para que no contenga información significativa. No tiene sentido usar la variable después de este bloque, por lo que es razonable abarcarla.

Otro ejemplo en el que puedo pensar es cuando tienes dos variables que tienen un nombre y un significado similares, pero trabajas con una y luego con otra, y quieres mantenerlas separadas. He escrito este código en C #:

{
    MethodBuilder m_ToString = tb.DefineMethod("ToString", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, typeofString, Type.EmptyTypes);
    var il = m_ToString.GetILGenerator();
    il.Emit(OpCodes.Ldstr, templateType.ToString()+":"+staticType.ToString());
    il.Emit(OpCodes.Ret);
    tb.DefineMethodOverride(m_ToString, m_Object_ToString);
}
{
    PropertyBuilder p_Class = tb.DefineProperty("Class", PropertyAttributes.None, typeofType, Type.EmptyTypes);
    MethodBuilder m_get_Class = tb.DefineMethod("get_Class", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, typeofType, Type.EmptyTypes);
    var il = m_get_Class.GetILGenerator();
    il.Emit(OpCodes.Ldtoken, staticType);
    il.Emit(OpCodes.Call, m_Type_GetTypeFromHandle);
    il.Emit(OpCodes.Ret);
    p_Class.SetGetMethod(m_get_Class);
    tb.DefineMethodOverride(m_get_Class, m_IPattern_get_Class);
}

Puede argumentar que simplemente puedo declarar ILGenerator il;en la parte superior del método, pero tampoco quiero reutilizar la variable para diferentes objetos (enfoque funcional). En este caso, los bloques facilitan la separación de los trabajos que se realizan, tanto sintácticamente como visualmente. También dice que después del bloqueo, he terminado ily nada debería acceder.

Un argumento en contra de este ejemplo es el uso de métodos. Tal vez sí, pero en este caso, el código no es tan largo, y separarlo en diferentes métodos también necesitaría transmitir todas las variables que se usan en el código.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.