Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Este tutorial le enseña varias características en .NET y el lenguaje C#. Aprenderá lo siguiente:
- Conceptos básicos de la CLI de .NET
- Estructura de una aplicación de consola de C#
- E/S de consola
- Conceptos básicos de las API de E/S de archivos en .NET
- Conceptos básicos de la programación asincrónica basada en tareas en .NET
Creará una aplicación que lee un archivo de texto y devuelve el contenido de ese archivo de texto a la consola. El ritmo de la salida a la consola se ajusta para que coincida con la lectura en voz alta. Puede acelerar o ralentizar el ritmo presionando las teclas "<" (menor que) o ">" (mayor que). Puede ejecutar esta aplicación en Windows, Linux, macOS o en un contenedor de Docker.
En este tutorial hay muchas características. Vamos a construirlos uno a uno.
Prerrequisitos
- La versión más reciente del SDK de .NET
- Editor de Visual Studio Code
- El DevKit de C#
Creación de la aplicación
El primer paso es crear una nueva aplicación. Abra un símbolo del sistema y cree un nuevo directorio para la aplicación. Conviértalo en el directorio actual. Escriba el comando dotnet new console
en el indicador de comandos. Esto crea los archivos de inicio para una aplicación básica "Hola mundo".
Antes de empezar a realizar modificaciones, vamos a ejecutar la sencilla aplicación Hello World. Después de crear la aplicación, escriba dotnet run
en el símbolo del sistema. Este comando ejecuta el proceso de restauración de paquetes NuGet, crea el ejecutable de la aplicación y ejecuta el ejecutable.
El código sencillo de la aplicación Hola mundo está todo en Program.cs. Abra ese archivo con su editor de texto favorito. Reemplace el código de Program.cs por el código siguiente:
namespace TeleprompterConsole;
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
En la parte superior del archivo, verá una instrucción namespace
. Al igual que otros lenguajes orientados a objetos que puede haber usado, C# usa espacios de nombres para organizar los tipos. Este programa Hola mundo no es diferente. Puede ver que el programa está en el espacio de nombres con el nombre TeleprompterConsole
.
Lectura y reflejo del archivo
La primera característica que se va a agregar es la capacidad de leer un archivo de texto y mostrar todo ese texto en la consola. En primer lugar, vamos a agregar un archivo de texto. Copie el archivo sampleQuotes.txt del repositorio de GitHub para esta de ejemplo en el directorio del proyecto. Esto servirá como script para la aplicación. Para obtener información sobre cómo descargar la aplicación de ejemplo de este tutorial, consulte las instrucciones de Ejemplos y tutoriales.
A continuación, agregue el método siguiente en la clase Program
(justo debajo del método Main
):
static IEnumerable<string> ReadFrom(string file)
{
string? line;
using (var reader = File.OpenText(file))
{
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}
Este método es un tipo especial de método de C# denominado método de iterador . Los métodos de iterador devuelven secuencias que se evalúan de forma diferida. Esto significa que cada elemento de la secuencia se genera tal y como lo solicita el código que consume la secuencia. Los métodos de iterador son métodos que contienen una o varias instrucciones yield return
. El objeto devuelto por el método ReadFrom
contiene el código para generar cada elemento de la secuencia. En este ejemplo, esto implica leer la siguiente línea de texto del archivo de origen y devolver esa cadena. Cada vez que el código de llamada solicita el siguiente elemento de la secuencia, el código lee la siguiente línea de texto del archivo y la devuelve. Cuando el archivo se lee por completo, la secuencia indica que no hay más elementos.
Hay dos elementos de sintaxis de C# que pueden ser nuevos para usted. La instrucción using
de este método administra la limpieza de recursos. La variable que se inicializa en la instrucción using
(reader
, en este ejemplo) debe implementar la interfaz IDisposable. Esa interfaz define un único método, Dispose
, al que se debe llamar cuando se debe liberar el recurso. El compilador genera esa llamada cuando la ejecución llega a la llave de cierre de la instrucción using
. El código que genera el compilador asegura que el recurso se libere incluso si se lanza una excepción desde el código en el bloque definido por la instrucción using.
La variable reader
se define mediante la palabra clave var
. var
define una variable local con tipo implícito. Esto significa que el tipo de la variable viene determinado por el tipo en tiempo de compilación del objeto asignado a la variable. Aquí, es el valor devuelto del método OpenText(String), que es un objeto StreamReader.
Ahora, vamos a rellenar el código para leer el archivo en el método Main
:
var lines = ReadFrom("sampleQuotes.txt");
foreach (var line in lines)
{
Console.WriteLine(line);
}
Ejecute el programa (con dotnet run
) y puede ver todas las líneas impresas en la consola.
Adición de retrasos y formato de salida
Lo que tiene se va a mostrar lejos, demasiado rápido para leerlo en voz alta. Ahora debe agregar los retrasos en la salida. A medida que empiece, creará parte del código principal que permite el procesamiento asincrónico. Sin embargo, a estos primeros pasos le seguirán algunos antipatrones. Los antipatrones se señalan en los comentarios cuando se agrega el código y el código se actualizará en los pasos posteriores.
Hay dos pasos para esta sección. Primero, actualizará el método Iterator para devolver palabras sueltas en lugar de líneas enteras. Eso se logra con estas modificaciones. Reemplace la instrucción yield return line;
por el código siguiente:
var words = line.Split(' ');
foreach (var word in words)
{
yield return word + " ";
}
yield return Environment.NewLine;
A continuación, debe modificar la forma en que se consumen las líneas del archivo y agregar una pausa después de escribir cada palabra. Reemplace la instrucción Console.WriteLine(line)
en el método Main
por el siguiente bloque:
Console.Write(line);
if (!string.IsNullOrWhiteSpace(line))
{
var pause = Task.Delay(200);
// Synchronously waiting on a task is an
// anti-pattern. This will get fixed in later
// steps.
pause.Wait();
}
Ejecute el ejemplo y compruebe la salida. Ahora, cada palabra única se imprime, seguida de un retraso de 200 ms. Sin embargo, la salida mostrada muestra algunos problemas porque el archivo de texto de origen tiene varias líneas que tienen más de 80 caracteres sin salto de línea. Este texto puede ser difícil de leer al desplazarse por él. Eso es fácil de corregir. Solo tendrá que realizar un seguimiento de la longitud de cada línea y generar una nueva línea cada vez que la longitud de la línea alcance un umbral determinado. Declare una variable local después de la declaración de words
en el método ReadFrom
que contenga la longitud de línea:
var lineLength = 0;
A continuación, agregue el código siguiente después de la instrucción yield return word + " ";
(antes de la llave de cierre):
lineLength += word.Length + 1;
if (lineLength > 70)
{
yield return Environment.NewLine;
lineLength = 0;
}
Ejecuta la muestra y podrás leer en voz alta a su ritmo preconfigurado.
Tareas asincrónicas
En este paso final, agregará el código para escribir la salida de forma asincrónica en una tarea, al tiempo que ejecutará otra tarea para leer la entrada del usuario si desea acelerar o ralentizar la presentación de texto o detener la presentación de texto por completo. Esto tiene algunos pasos en él y, al final, tendrá todas las actualizaciones que necesita. El primer paso es crear un método de devolución de Task asincrónico que represente el código que ha creado hasta ahora para leer y mostrar el archivo.
Añada este método a su clase Program
(tomado del cuerpo de su método Main
):
private static async Task ShowTeleprompter()
{
var words = ReadFrom("sampleQuotes.txt");
foreach (var word in words)
{
Console.Write(word);
if (!string.IsNullOrWhiteSpace(word))
{
await Task.Delay(200);
}
}
}
Observará dos cambios. En primer lugar, en el cuerpo del método, en lugar de llamar a Wait() esperar sincrónicamente a que finalice una tarea, esta versión usa la palabra clave await
. Para ello, debe agregar el modificador async
a la firma del método. Este método devuelve un Task
. Observe que no hay instrucciones return que devuelvan un objeto Task
. En su lugar, ese objeto Task
se crea mediante código que genera el compilador cuando se usa el operador await
. Puede imaginar que este método devuelve un valor cuando alcanza un await
. El Task
devuelto indica que el trabajo no se ha completado. El método se reanuda cuando se completa la tarea esperada. Cuando se ha ejecutado hasta la finalización, el Task
devuelto indica que se ha completado.
El código de llamada puede supervisar ese valor de Task
devuelto para determinar cuándo se ha completado.
Agregue una palabra clave await
antes de la llamada a ShowTeleprompter
:
await ShowTeleprompter();
Esto requiere que cambie la firma del método Main
a:
static async Task Main(string[] args)
Obtenga más información sobre el método async Main
en nuestra sección aspectos básicos.
A continuación, debe escribir el segundo método asincrónico para leer desde la consola y detectar las teclas '<' (menor que), '>' (mayor que) y 'X' o 'x'. Este es el método que agrega para esa tarea:
private static async Task GetInput()
{
var delay = 200;
Action work = () =>
{
do {
var key = Console.ReadKey(true);
if (key.KeyChar == '>')
{
delay -= 10;
}
else if (key.KeyChar == '<')
{
delay += 10;
}
else if (key.KeyChar == 'X' || key.KeyChar == 'x')
{
break;
}
} while (true);
};
await Task.Run(work);
}
Esto crea una expresión lambda para representar un delegado de Action que lee una clave de la Consola y modifica una variable local que representa el retraso cuando el usuario presiona las teclas "<" (menor que) o ">" (mayor que). El método delegado finaliza cuando el usuario presiona las teclas "X" o "x", lo que permite al usuario detener la presentación del texto en cualquier momento. Este método usa ReadKey() para bloquear y esperar a que el usuario presione una tecla.
Para finalizar esta característica, debe crear un nuevo método de devolución de async Task
que inicie ambas tareas (GetInput
y ShowTeleprompter
) y también administre los datos compartidos entre estas dos tareas.
Es el momento de crear una clase que pueda controlar los datos compartidos entre estas dos tareas. Esta clase contiene dos propiedades públicas: el retraso y una marca Done
para indicar que el archivo se ha leído completamente:
namespace TeleprompterConsole;
internal class TelePrompterConfig
{
public int DelayInMilliseconds { get; private set; } = 200;
public void UpdateDelay(int increment) // negative to speed up
{
var newDelay = Min(DelayInMilliseconds + increment, 1000);
newDelay = Max(newDelay, 20);
DelayInMilliseconds = newDelay;
}
public bool Done { get; private set; }
public void SetDone()
{
Done = true;
}
}
Coloque esa clase en un nuevo archivo e incluya esa clase en el espacio de nombres TeleprompterConsole
tal como se muestra. También deberá agregar una instrucción using static
en la parte superior del archivo para que pueda hacer referencia a los métodos Min
y Max
sin la clase incluida o los nombres de espacio de nombres. Una instrucción using static
importa los métodos de una clase. Esto contrasta con la instrucción using
sin static
, que importa todas las clases de un espacio de nombres.
using static System.Math;
A continuación, debe actualizar los métodos ShowTeleprompter
y GetInput
para usar el nuevo objeto config
. Escriba un método final Task
que devuelva async
para iniciar ambas tareas y salir cuando finalice la primera tarea.
private static async Task RunTeleprompter()
{
var config = new TelePrompterConfig();
var displayTask = ShowTeleprompter(config);
var speedTask = GetInput(config);
await Task.WhenAny(displayTask, speedTask);
}
El método nuevo aquí es la llamada WhenAny(Task[]). Esto crea un Task
que finaliza tan pronto como se complete cualquiera de las tareas de su lista de argumentos.
A continuación, debe actualizar los métodos ShowTeleprompter
y GetInput
para usar el objeto config
para el retraso:
private static async Task ShowTeleprompter(TelePrompterConfig config)
{
var words = ReadFrom("sampleQuotes.txt");
foreach (var word in words)
{
Console.Write(word);
if (!string.IsNullOrWhiteSpace(word))
{
await Task.Delay(config.DelayInMilliseconds);
}
}
config.SetDone();
}
private static async Task GetInput(TelePrompterConfig config)
{
Action work = () =>
{
do {
var key = Console.ReadKey(true);
if (key.KeyChar == '>')
config.UpdateDelay(-10);
else if (key.KeyChar == '<')
config.UpdateDelay(10);
else if (key.KeyChar == 'X' || key.KeyChar == 'x')
config.SetDone();
} while (!config.Done);
};
await Task.Run(work);
}
Esta nueva versión de ShowTeleprompter
llama a un nuevo método en la clase TeleprompterConfig
. Ahora, debe actualizar Main
para llamar a RunTeleprompter
en lugar de ShowTeleprompter
:
await RunTeleprompter();
Conclusión
En este tutorial se muestran varias características en torno al lenguaje C# y las bibliotecas de .NET Core relacionadas con el trabajo en aplicaciones de consola. Puede basarse en este conocimiento para explorar más sobre el lenguaje y las clases que se presentan aquí. Ha visto los conceptos básicos de E/S de archivo y consola, el bloqueo y el uso sin bloqueo de la programación asincrónica basada en tareas, un paseo por el lenguaje C# y cómo se organizan los programas de C# y la CLI de .NET.
Para obtener más información sobre la E/S de archivos y secuencias, consulte File and Stream I/O. Para obtener más información sobre el modelo de programación asincrónica usado en este tutorial, consulte programación asincrónica basada en tareas y programación asincrónica .