c# - keywords - meta tags seo
¿Para qué se utiliza la palabra clave de rendimiento en C#? (16)
A primera vista, el rendimiento es un azúcar .NET para devolver un IEnumerable.
Sin rendimiento, todos los elementos de la colección se crean a la vez:
class SomeData
{
public SomeData() { }
static public IEnumerable<SomeData> CreateSomeDatas()
{
return new List<SomeData> {
new SomeData(),
new SomeData(),
new SomeData()
};
}
}
Mismo código usando rendimiento, devuelve artículo por artículo:
class SomeData
{
public SomeData() { }
static public IEnumerable<SomeData> CreateSomeDatas()
{
yield return new SomeData();
yield return new SomeData();
yield return new SomeData();
}
}
La ventaja de utilizar el rendimiento es que si la función que consume sus datos simplemente necesita el primer elemento de la colección, el resto de los elementos no se crearán.
El operador de rendimiento permite la creación de artículos a medida que se demanda. Esa es una buena razón para usarlo.
En la pregunta ¿Cómo puedo exponer solo un fragmento de IList <> una de las respuestas tenía el siguiente fragmento de código:
IEnumerable<object> FilteredList()
{
foreach( object item in FullList )
{
if( IsItemInPartialList( item )
yield return item;
}
}
¿Qué hace la palabra clave de rendimiento allí? Lo he visto en un par de lugares, y en otra pregunta, pero no he descubierto lo que realmente hace. Estoy acostumbrado a pensar en rendimiento en el sentido de que un hilo cede a otro, pero eso no parece relevante aquí.
Aquí hay una forma sencilla de entender el concepto: la idea básica es, si desea una colección en la que pueda usar " foreach
", pero reunir los elementos en la colección es costoso por alguna razón (como consultarlos en una base de datos) Y, a menudo, no necesitará la colección completa, luego creará una función que construye la colección un elemento a la vez y se la devuelve al consumidor (quien puede terminar el esfuerzo de la colección antes).
Piénsalo de esta manera: vas al mostrador de carne y quieres comprar una libra de jamón rebanado. El carnicero lleva un jamón de 10 libras a la parte posterior, lo coloca en la máquina rebanadora, lo rebana por completo, luego le trae el montón de rebanadas y mide una libra. (Vieja forma). Con el yield
, el carnicero lleva la máquina rebanadora al mostrador, y comienza a cortar y "ceder" cada rebanada a la escala hasta que mida 1 libra, luego la envuelve y listo. The Old Way puede ser mejor para el carnicero (le permite organizar su maquinaria de la manera que más le guste), pero New Way es claramente más eficiente en la mayoría de los casos para el consumidor.
De manera intuitiva, la palabra clave devuelve un valor de la función sin dejarla, es decir, en su ejemplo de código, devuelve el valor del item
actual y luego reanuda el ciclo. Más formalmente, el compilador lo utiliza para generar código para un iterador . Los iteradores son funciones que devuelven objetos IEnumerable
. El MSDN tiene varios articles sobre ellos.
El rendimiento tiene dos grandes usos,
Ayuda a proporcionar una iteración personalizada sin crear colecciones temporales.
Para explicar más arriba dos puntos más demostrativos, he creado un video simple que puede ver here
Es una forma muy simple y fácil de crear un enumerable para su objeto. El compilador crea una clase que envuelve su método y que implementa, en este caso, IEnumerable <object>. Sin la palabra clave de rendimiento, tendría que crear un objeto que implementa IEnumerable <objeto>.
Está produciendo secuencia enumerable. Lo que hace es crear una secuencia IEnumerable local y devolverla como resultado del método.
Está tratando de traer algo de Ruby Goodness :)
Concepto: Este es un ejemplo de código de Ruby que imprime cada elemento de la matriz
rubyArray = [1,2,3,4,5,6,7,8,9,10]
rubyArray.each{|x|
puts x # do whatever with x
}
La implementación de cada método de la Matriz le otorga control a la persona que llama (el ''pone x'') con cada elemento de la matriz claramente presentado como x. La persona que llama puede hacer lo que tenga que hacer con x.
Sin embargo, .Net no llega hasta aquí ... C # parece haber acoplado el rendimiento con IEnumerable, en una forma que lo obliga a escribir un bucle foreach en la persona que llama como se ve en la respuesta de Mendelt. Poco menos elegante.
//calling code
foreach(int i in obCustomClass.Each())
{
Console.WriteLine(i.ToString());
}
// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
for(int iLooper=0; iLooper<data.Length; ++iLooper)
yield return data[iLooper];
}
Este link tiene un ejemplo simple.
Incluso ejemplos más simples están aquí
public static IEnumerable<int> testYieldb()
{
for(int i=0;i<3;i++) yield return 4;
}
Observe que la rentabilidad no regresará del método. Incluso puedes poner un WriteLine
después de la yield return
Lo anterior produce un IEnumerable de 4 ints 4,4,4,4.
Aquí con una línea de WriteLine
. Agregará 4 a la lista, imprimirá abc, luego agregará 4 a la lista, luego completará el método y, por lo tanto, regresará del método (una vez que el método se haya completado, como sucedería con un procedimiento sin devolución). Pero esto tendría un valor, una lista IEnumerable
de int
s, que se devuelve al finalizar.
public static IEnumerable<int> testYieldb()
{
yield return 4;
console.WriteLine("abc");
yield return 4;
}
Tenga en cuenta también que cuando utiliza el rendimiento, lo que está devolviendo no es del mismo tipo que la función. Es del tipo de un elemento dentro de la lista IEnumerable
.
Se utiliza el rendimiento con el tipo de retorno del método como IEnumerable
. Si el tipo de retorno del método es int
o List<int>
y utiliza el yield
, no se compilará. Puede usar el tipo de retorno del método IEnumerable
sin rendimiento, pero parece que tal vez no pueda usar el rendimiento sin el tipo de retorno del método IEnumerable
.
Y para que lo ejecutes hay que llamarlo de una manera especial.
static void Main(string[] args)
{
testA();
Console.Write("try again. the above won''t execute any of the function!/n");
foreach (var x in testA()) { }
Console.ReadLine();
}
// static List<int> testA()
static IEnumerable<int> testA()
{
Console.WriteLine("asdfa");
yield return 1;
Console.WriteLine("asdf");
}
Iteración. Crea una máquina de estado "debajo de las cubiertas" que recuerda dónde se encontraba en cada ciclo adicional de la función y continúa desde allí.
La palabra clave de yield
permite crear una IEnumerable<T>
en el formulario en un bloque iterador . Este bloque de iteradores admite la ejecución diferida y, si no está familiarizado con el concepto, puede parecer casi mágico. Sin embargo, al final del día es solo un código que se ejecuta sin trucos extraños.
Un bloque de iteradores se puede describir como azúcar sintáctica donde el compilador genera una máquina de estados que realiza un seguimiento de cuánto ha progresado la enumeración de los enumerables. Para enumerar un enumerable, a menudo utiliza un bucle foreach
. Sin embargo, un bucle foreach
también es azúcar sintáctico. Entonces, son dos abstracciones eliminadas del código real, por lo que inicialmente puede ser difícil entender cómo funciona todo junto.
Supongamos que tiene un bloque iterador muy simple:
IEnumerable<int> IteratorBlock()
{
Console.WriteLine("Begin");
yield return 1;
Console.WriteLine("After 1");
yield return 2;
Console.WriteLine("After 2");
yield return 42;
Console.WriteLine("End");
}
Los bloques de iteradores reales a menudo tienen condiciones y bucles, pero cuando verifica las condiciones y desenrolla los bucles, todavía terminan como declaraciones de yield
intercaladas con otro código.
Para enumerar el bloque iterador se usa un bucle foreach
:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Aquí está la salida (sin sorpresas aquí):
Begin 1 After 1 2 After 2 42 End
Como se indicó anteriormente para cada uno es azúcar sintáctica:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
En un intento de desenredar esto, he creado un diagrama de secuencia con las abstracciones eliminadas:
La máquina de estado generada por el compilador también implementa el enumerador, pero para aclarar el diagrama, los he mostrado como instancias separadas. (Cuando se enumera la máquina de estado de otro hilo, en realidad se obtienen instancias separadas, pero ese detalle no es importante aquí).
Cada vez que llama a su bloque iterador, se crea una nueva instancia de la máquina de estado. Sin embargo, ninguno de sus códigos en el bloque de iteradores se ejecuta hasta que el enumerator.MoveNext()
ejecuta por primera vez. Así es como funciona la ejecución diferida. Aquí hay un ejemplo (bastante tonto):
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
En este punto el iterador no se ha ejecutado. La cláusula Where
crea un nuevo IEnumerable<T>
que envuelve el IEnumerable<T>
devuelto por IteratorBlock
pero este enumerable aún no se ha enumerado. Esto sucede cuando ejecutas un bucle foreach
:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Si enumera la enumeración dos veces, se creará una nueva instancia de la máquina de estado cada vez y su bloque de iteradores ejecutará el mismo código dos veces.
Tenga en cuenta que los métodos LINQ como ToList()
, ToArray()
, First()
, Count()
etc. usarán un bucle foreach
para enumerar el enumerable. Por ejemplo, ToList()
enumerará todos los elementos del enumerable y los almacenará en una lista. Ahora puede acceder a la lista para obtener todos los elementos del enumerable sin que se ejecute nuevamente el bloque iterador. Existe un compromiso entre el uso de la CPU para producir los elementos de la enumeración varias veces y la memoria para almacenar los elementos de la enumeración para acceder a ellos varias veces cuando se usan métodos como ToList()
.
La palabra clave de rendimiento de C #, para decirlo simplemente, permite muchas llamadas a un cuerpo de código, conocido como un iterador, que sabe cómo regresar antes de que se termine y, cuando se vuelve a llamar, continúa donde lo dejó, es decir, ayuda a un iterador se convierte de forma transparente y con estado para cada elemento en una secuencia que el iterador devuelve en llamadas sucesivas.
En JavaScript, el mismo concepto se llama Generadores.
La palabra clave de rendimiento en realidad hace bastante aquí. La función devuelve un objeto que implementa la interfaz IEnumerable. Si una función que llama comienza a buscarse sobre este objeto, se vuelve a llamar a la función hasta que "cede". Este es el azúcar sintáctico introducido en C # 2.0. En versiones anteriores tenías que crear tus propios objetos IEnumerable e IEnumerator para hacer cosas como esta.
La forma más fácil de entender un código como este es escribir un ejemplo, establecer algunos puntos de interrupción y ver qué sucede.
Intenta pasar por esto, por ejemplo:
public void Consumer()
{
foreach(int i in Integers())
{
Console.WriteLine(i.ToString());
}
}
public IEnumerable<int> Integers()
{
yield return 1;
yield return 2;
yield return 4;
yield return 8;
yield return 16;
yield return 16777216;
}
Cuando recorre el ejemplo, encontrará que la primera llamada a enteros () devuelve 1. La segunda llamada devuelve 2 y la línea "rendimiento rendimiento 1" no se ejecuta de nuevo.
Aquí hay un ejemplo de la vida real.
public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
using (var connection = CreateConnection())
{
using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
{
command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
yield return make(reader);
}
}
}
}
}
Recientemente, Raymond Chen también publicó una interesante serie de artículos sobre la palabra clave de rendimiento.
- blogs.msdn.com/oldnewthing/archive/2008/08/12/8849519.aspx
- La implementación de iteradores en C # y sus consecuencias (parte 2)
- La implementación de iteradores en C # y sus consecuencias (parte 3)
- La implementación de iteradores en C # y sus consecuencias (parte 4)
Si bien se usa nominalmente para implementar fácilmente un patrón de iterador, pero se puede generalizar en una máquina de estado. No tiene sentido citar a Raymond, la última parte también se vincula a otros usos (pero el ejemplo en el blog de Entin es especialmente bueno, y muestra cómo escribir código seguro asíncrono).
Si entiendo esto correctamente, así es como lo expresaría desde la perspectiva de la función que implementa IEnumerable con el rendimiento.
- Aquí hay uno.
- Llama de nuevo si necesitas otra.
- Recordaré lo que ya te di.
- Solo sabré si puedo darte otro cuando llames de nuevo.
Una implementación de lista o matriz carga todos los elementos inmediatamente, mientras que la implementación de rendimiento proporciona una solución de ejecución diferida.
En la práctica, a menudo es conveniente realizar la cantidad mínima de trabajo según sea necesario para reducir el consumo de recursos de una aplicación.
Por ejemplo, podemos tener una aplicación que procesa millones de registros de una base de datos. Se pueden lograr los siguientes beneficios cuando usamos IEnumerable en un modelo basado en extracción diferida de ejecución:
- Es probable que la escalabilidad, la confiabilidad y la previsibilidad mejoren, ya que la cantidad de registros no afecta significativamente los requisitos de recursos de la aplicación.
- Es probable que el rendimiento y la capacidad de respuesta mejoren, ya que el procesamiento puede comenzar de inmediato en lugar de esperar a que se cargue primero toda la colección.
- Es probable que la capacidad de recuperación y la utilización mejoren, ya que la aplicación se puede detener, iniciar, interrumpir o fallar. Solo los elementos en progreso se perderán en comparación con la obtención previa de todos los datos en los que solo se usó una parte de los resultados.
- El procesamiento continuo es posible en entornos donde se agregan flujos de carga de trabajo constante.
Aquí hay una comparación entre crear una colección primero, como una lista en comparación con el uso de rendimiento.
Ejemplo de lista
public class ContactListStore : IStore<ContactModel>
{
public IEnumerable<ContactModel> GetEnumerator()
{
var contacts = new List<ContactModel>();
Console.WriteLine("ContactListStore: Creating contact 1");
contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
Console.WriteLine("ContactListStore: Creating contact 2");
contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
Console.WriteLine("ContactListStore: Creating contact 3");
contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
return contacts;
}
}
static void Main(string[] args)
{
var store = new ContactListStore();
var contacts = store.GetEnumerator();
Console.WriteLine("Ready to iterate through the collection.");
Console.ReadLine();
}
Salida de consola
ContactListStore: Creando contacto 1
ContactListStore: Creando contacto 2
ContactListStore: Creando contacto 3
Listo para iterar a través de la colección.
Nota: la colección completa se cargó en la memoria sin siquiera solicitar un solo elemento de la lista
Ejemplo de rendimiento
public class ContactYieldStore : IStore<ContactModel>
{
public IEnumerable<ContactModel> GetEnumerator()
{
Console.WriteLine("ContactYieldStore: Creating contact 1");
yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
Console.WriteLine("ContactYieldStore: Creating contact 2");
yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
Console.WriteLine("ContactYieldStore: Creating contact 3");
yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
}
}
static void Main(string[] args)
{
var store = new ContactYieldStore();
var contacts = store.GetEnumerator();
Console.WriteLine("Ready to iterate through the collection.");
Console.ReadLine();
}
Salida de consola
Listo para iterar a través de la colección.
Nota: La colección no fue ejecutada en absoluto. Esto se debe a la naturaleza de "ejecución diferida" de IEnumerable. La construcción de un elemento solo se producirá cuando sea realmente necesario.
Llamemos a la colección de nuevo y compensemos el comportamiento cuando obtengamos el primer contacto de la colección.
static void Main(string[] args)
{
var store = new ContactYieldStore();
var contacts = store.GetEnumerator();
Console.WriteLine("Ready to iterate through the collection");
Console.WriteLine("Hello {0}", contacts.First().FirstName);
Console.ReadLine();
}
Salida de consola
Listo para recorrer la colección.
ContactYieldStore: Creando contacto 1
Hola Bob
¡Bonito! Solo el primer contacto se construyó cuando el cliente "sacó" el artículo de la colección.
yield return
se utiliza con los enumeradores. En cada llamada de declaración de rendimiento, el control se devuelve a la persona que llama, pero garantiza que se mantenga el estado de la persona que llama. Debido a esto, cuando el llamante enumera el siguiente elemento, continúa la ejecución en el método del destinatario desde la declaración inmediatamente después de la declaración de yield
.
Tratemos de entender esto con un ejemplo. En este ejemplo, correspondiente a cada línea, he mencionado el orden en que fluye la ejecución.
static void Main(string[] args)
{
foreach (int fib in Fibs(6))//1, 5
{
Console.WriteLine(fib + " ");//4, 10
}
}
static IEnumerable<int> Fibs(int fibCount)
{
for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
{
yield return prevFib;//3, 9
int newFib = prevFib + currFib;//6
prevFib = currFib;//7
currFib = newFib;//8
}
}
Además, el estado se mantiene para cada enumeración. Supongamos que tengo otra llamada al método Fibs()
, entonces el estado se restablecerá para ello.