¿Es mejor declarar una variable dentro o fuera de un bucle?

Es mejor hacer:

variable1Type foo; variable2Type baa; foreach(var val in list) { foo = new Foo( ... ); foo.x = FormatValue(val); baa = new Baa(); baa.main = foo; baa.Do(); } 

O:

 foreach(var val in list) { variable1Type foo = new Foo( ... ); foo.x = FormatValue(val); variable2Type baa = new Baa(); baa.main = foo; baa.Do(); } 

La pregunta es: ¿qué es más rápido 1 caso o 2 casos? ¿La diferencia es insignificante? ¿Es lo mismo en aplicaciones reales? Esto puede ser un micro de optimización, pero realmente quiero saber qué es mejor.

En cuanto a rendimiento, probemos ejemplos concretos:

 public void Method1() { foreach(int i in Enumerable.Range(0, 10)) { int x = i * i; StringBuilder sb = new StringBuilder(); sb.Append(x); Console.WriteLine(sb); } } public void Method2() { int x; StringBuilder sb; foreach(int i in Enumerable.Range(0, 10)) { x = i * i; sb = new StringBuilder(); sb.Append(x); Console.WriteLine(sb); } } 

Deliberadamente elegí tanto un tipo de valor como un tipo de referencia en caso de que eso afecte las cosas. Ahora, la IL para ellos:

 .method public hidebysig instance void Method1() cil managed { .maxstack 2 .locals init ( [0] int32 i, [1] int32 x, [2] class [mscorlib]System.Text.StringBuilder sb, [3] class [mscorlib]System.Collections.Generic.IEnumerator`1 enumerator) L_0000: ldc.i4.0 L_0001: ldc.i4.s 10 L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1 [System.Core]System.Linq.Enumerable::Range(int32, int32) L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1 [mscorlib]System.Collections.Generic.IEnumerable`1::GetEnumerator() L_000d: stloc.3 L_000e: br.s L_002f L_0010: ldloc.3 L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1::get_Current() L_0016: stloc.0 L_0017: ldloc.0 L_0018: ldloc.0 L_0019: mul L_001a: stloc.1 L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor() L_0020: stloc.2 L_0021: ldloc.2 L_0022: ldloc.1 L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32) L_0028: pop L_0029: ldloc.2 L_002a: call void [mscorlib]System.Console::WriteLine(object) L_002f: ldloc.3 L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() L_0035: brtrue.s L_0010 L_0037: leave.s L_0043 L_0039: ldloc.3 L_003a: brfalse.s L_0042 L_003c: ldloc.3 L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose() L_0042: endfinally L_0043: ret .try L_000e to L_0039 finally handler L_0039 to L_0043 } .method public hidebysig instance void Method2() cil managed { .maxstack 2 .locals init ( [0] int32 x, [1] class [mscorlib]System.Text.StringBuilder sb, [2] int32 i, [3] class [mscorlib]System.Collections.Generic.IEnumerator`1 enumerator) L_0000: ldc.i4.0 L_0001: ldc.i4.s 10 L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1 [System.Core]System.Linq.Enumerable::Range(int32, int32) L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1 [mscorlib]System.Collections.Generic.IEnumerable`1::GetEnumerator() L_000d: stloc.3 L_000e: br.s L_002f L_0010: ldloc.3 L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1::get_Current() L_0016: stloc.2 L_0017: ldloc.2 L_0018: ldloc.2 L_0019: mul L_001a: stloc.0 L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor() L_0020: stloc.1 L_0021: ldloc.1 L_0022: ldloc.0 L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32) L_0028: pop L_0029: ldloc.1 L_002a: call void [mscorlib]System.Console::WriteLine(object) L_002f: ldloc.3 L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() L_0035: brtrue.s L_0010 L_0037: leave.s L_0043 L_0039: ldloc.3 L_003a: brfalse.s L_0042 L_003c: ldloc.3 L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose() L_0042: endfinally L_0043: ret .try L_000e to L_0039 finally handler L_0039 to L_0043 } 

Como puede ver, aparte del orden en la stack que eligió el comstackdor, que podría haber sido un orden diferente, no tuvo ningún efecto. A su vez, realmente no hay nada que le esté dando la trepidación para hacer mucho uso de lo que el otro no le está dando.

Aparte de eso, hay un tipo de diferencia.

En mi Method1() , x y sb tienen el scope del foreach y no se puede acceder ni deliberadamente ni accidentalmente fuera de él.

En mi Method2() , x y sb no se conocen en tiempo de comstackción para que se les asigne un valor dentro del foreach (el comstackdor no sabe que foreach realizará al menos un bucle), por lo que está prohibido su uso.

Hasta ahora, no hay diferencia real.

Sin embargo, puedo asignar y usar x y / o sb fuera del foreach . Como regla, diría que probablemente este sea un ámbito pobre la mayor parte del tiempo, por lo que preferiría el Method1 , pero podría tener alguna razón sensata para querer referirme a ellos (de manera más realista si no estuvieran posiblemente sin asignar), en qué caso Method2 .

Aún así, esa es una cuestión de cómo se puede extender o no cada código, no una diferencia del código tal como está escrito. Realmente, no hay diferencia.

No importa, no tiene ningún efecto en el rendimiento en absoluto.

pero realmente quiero saber hacer de manera correcta.

La mayoría te dirá que dentro del ciclo tiene más sentido.

Es solo una cuestión de scope. En este caso, donde foo y baa solo se usan dentro del bucle for, es una buena práctica declararlos dentro del bucle. Es más seguro también.

De acuerdo, respondí esto sin darme cuenta de que el póster original creaba un objeto nuevo cada vez que pasaba por el ciclo y no simplemente “usar” el objeto. Entonces, no, no debería haber una diferencia insignificante cuando se trata de rendimiento. Con esto, iría con el segundo método y declararía el objeto dentro del ciclo. De esta forma, se limpiará durante el próximo pase de GC y mantendrá el objeto dentro del scope.

— Dejaré mi respuesta original, simplemente porque lo escribí todo, y podría ayudar a alguien más que busque este mensaje más adelante. Prometo que en el futuro prestaré más atención antes de intentar responder la siguiente pregunta.

Tim

En realidad, creo que hay una diferencia. Si recuerdo correctamente, cada vez que creas un nuevo objeto = new foo() obtendrás ese objeto agregado al montón de memoria. Por lo tanto, al crear los objetos en el bucle, se agregarán a la sobrecarga del sistema. Si sabes que el ciclo va a ser pequeño, no es un problema.

Por lo tanto, si termina en un bucle con decir 1000 objetos en él, va a crear 1000 variables que no se eliminarán hasta la próxima recolección de basura. Ahora acceda a una base de datos donde desea hacer algo y tiene más de 20,000 filas para trabajar … Puede crear una demanda de sistema bastante grande según el tipo de objeto que esté creando.

Esto debería ser fácil de probar … Cree una aplicación que cree 10,000 elementos con un sello de tiempo cuando ingrese al bucle y cuando salga. La primera vez que lo haga, declare la variable antes del ciclo y la próxima vez durante el ciclo. Sin embargo, es posible que deba boost esa cuenta mucho más alta que 10K para ver una diferencia real en la velocidad.

Además, está el problema del scope. Si se crea en el ciclo, desaparecerá una vez que salga del ciclo, por lo que no podrá acceder a él nuevamente. Pero esto también ayuda con la limpieza ya que la recolección de basura eventualmente se deshará de ella una vez que salga del ciclo.

Tim

Ambos son completamente válidos, no estoy seguro de que haya una “forma correcta” de hacerlo.

Su primer caso es más eficiente con la memoria (al menos a corto plazo). Declarar sus variables dentro del bucle obligará a una mayor reasignación de la memoria; sin embargo, con el recolector de basura de .NET, cuando esas variables se salgan del scope, se limpiarán periódicamente, pero no necesariamente de inmediato. La diferencia de velocidad es posiblemente insignificante.

El segundo caso es de hecho un poco más seguro, ya que limitar el scope de sus variables tanto como sea posible suele ser una buena práctica.

En JS, la asignación de la memoria es completa cada vez. En C #, normalmente no existe esa diferencia, pero si la variable local es capturada por un método anónimo como la expresión lambda, será importante.