tema 3: ficheros 2 conceptos bÁsicos 2 … de materias y libros/libros todos...mismo nombre siempre...

38
TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 RUTAS DE FICHEROS Y DIRECTORIOS 3 SINTAXIS EN WINDOWS 3 RUTAS INDEPENDIENTES DEL SISTEMA OPERATIVO 4 OPERACIONES CON RUTAS 5 REPRESENTACIÓN DE FICHEROS Y DIRECTORIOS EN .NET 6 ELEMENTOS COMUNES 6 ELEMENTOS ESPECÍFICOS DE DIRECTORIOS 9 ELEMENTOS ESPECÍFICOS DE FICHEROS 10 FLUJOS DE ENTRADA-SALIDA 11 LECTURA Y ESCRITURA 12 MOVIMIENTO POR EL FLUJO 12 VOLCADO DE DATOS EN FLUJOS 14 CIERRE DE FLUJOS 14 FLUJOS DE ENTRADA-SALIDA EN FICHEROS 14 CREACIÓN DE OBJETOS FILESTREAM 14 MANIPULACIÓN DEL CONTENIDO DE LOS FICHEROS 16 CONTROL DE CONCURRENCIA EN ACCESOS A FICHEROS 19 ACCESO NATIVO A FICHEROS 20 FICHERO BINARIOS 20 FICHEROS DE TEXTO 22 LECTURA DE FICHEROS DE TEXTO 22 ESCRITURA EN FICHEROS DE TEXTO 25 MANIPULACIÓN DEL SISTEMA DE ARCHIVOS 26 MANIPULACIÓN DE FICHEROS 27 MANIPULACIÓN DE DIRECTORIOS 28 MANIPULACIÓN DE RUTAS 30 DETECCIÓN DE CAMBIOS EN EL SISTEMA DE ARCHIVOS 30 SELECCIÓN DE FICHEROS A VIGILAR 31 SELECCIÓN DE CAMBIOS A VIGILAR 32 DETECCIÓN SÍNCRONA DE CAMBIOS 32 DETECCIÓN ASÍNCRONA DE CAMBIOS 34 EJEMPLO: CIFRADOR DE DIRECTORIOS 35 PROBLEMAS DE DESBORDAMIENTOS DEL BUFFER DE CAMBIOS 36 FICHEROS TEMPORALES 37

Upload: others

Post on 17-Jun-2020

0 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

TEMA 3: FICHEROS 2

CONCEPTOS BÁSICOS 2 RUTAS DE FICHEROS Y DIRECTORIOS 3 SINTAXIS EN WINDOWS 3 RUTAS INDEPENDIENTES DEL SISTEMA OPERATIVO 4 OPERACIONES CON RUTAS 5 REPRESENTACIÓN DE FICHEROS Y DIRECTORIOS EN .NET 6 ELEMENTOS COMUNES 6 ELEMENTOS ESPECÍFICOS DE DIRECTORIOS 9 ELEMENTOS ESPECÍFICOS DE FICHEROS 10 FLUJOS DE ENTRADA-SALIDA 11 LECTURA Y ESCRITURA 12 MOVIMIENTO POR EL FLUJO 12 VOLCADO DE DATOS EN FLUJOS 14 CIERRE DE FLUJOS 14 FLUJOS DE ENTRADA-SALIDA EN FICHEROS 14 CREACIÓN DE OBJETOS FILESTREAM 14 MANIPULACIÓN DEL CONTENIDO DE LOS FICHEROS 16 CONTROL DE CONCURRENCIA EN ACCESOS A FICHEROS 19 ACCESO NATIVO A FICHEROS 20 FICHERO BINARIOS 20 FICHEROS DE TEXTO 22 LECTURA DE FICHEROS DE TEXTO 22 ESCRITURA EN FICHEROS DE TEXTO 25 MANIPULACIÓN DEL SISTEMA DE ARCHIVOS 26 MANIPULACIÓN DE FICHEROS 27 MANIPULACIÓN DE DIRECTORIOS 28 MANIPULACIÓN DE RUTAS 30 DETECCIÓN DE CAMBIOS EN EL SISTEMA DE ARCHIVOS 30 SELECCIÓN DE FICHEROS A VIGILAR 31 SELECCIÓN DE CAMBIOS A VIGILAR 32 DETECCIÓN SÍNCRONA DE CAMBIOS 32 DETECCIÓN ASÍNCRONA DE CAMBIOS 34 EJEMPLO: CIFRADOR DE DIRECTORIOS 35 PROBLEMAS DE DESBORDAMIENTOS DEL BUFFER DE CAMBIOS 36 FICHEROS TEMPORALES 37

Page 2: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Tema 3: Ficheros

Conceptos básicos Un fichero puede verse como una porción de un dispositivo de almacenamiento no volátil (disco duro, disquete, etc.) a la que se le asocia un determinado nombre, estando en principio la cantidad de datos que puede almacenar sólo limitada por la cantidad de espacio del que disponga en cada momento el dispositivo donde se almacenen esos datos o por las características del cada sistema operativo. Por no volátil se entiende que a diferencia de lo que ocurre con otros almacenes de datos como la memoria RAM, la información en ellos almacenadas no se pierde al apagarse el ordenador. Dada la importancia de los ficheros como almacenes no volátiles de la información, la BCL incluye todo un espacio de nombres llamado System.IO especialmente orientado al trabajo con ellos. En este tema se realizará un estudio en profundidad del mismo y se explicará cómo se pueden aprovechar los servicios que sus tipos ofrecen para facilitar la manipulación de los mismos. Por ello, salvo que se indique explícitamente lo contrario puede considerar que todos los nuevos tipos aquí citados forman parte de dicho espacio. Aunque cada sistema operativo puede tener su propio formato de nombres de fichero, nosotros veremos el que se utiliza en la plataforma .NET, que básicamente puede considerarse que consiste en nombrar a los ficheros usando hasta 259 caracteres Unicode imprimibles e interpretándose el nombre que se les dé según este formato: <nombre>.<extensión> <nombre> indica cuál ha de considerarse que es en realidad el nombre del fichero y en <extensión> se indica cuál es el tipo de fichero del que se trata. La idea es que ficheros con el mismo tipo de contenido tengan una extensión común para que así sea más fácil identificar su contenido, por lo que lo que distinguirá a unos ficheros de un tipo de otros de su mismo tipo será su <nombre> Así, los ficheros de texto generados por Microsoft Word tienen extensión doc, las compresiones con Winzip tienen extensión zip, etc. Los ficheros se agrupan en directorios o carpetas, que pueden verse simplemente como nombres comunes bajo los que se agrupan conjuntos de ficheros relacionados entre sí. Cada directorio pueden contener a su vez otros directorios, lo que hace que el sistema de archivos adquiera una estructura jerárquica donde cada fichero o directorio tiene como padre al directorio en que está contenido. Obviamente, para que ésta sea una estructura finita habrá de existir un directorio raíz que contenga a todos los demás y no esté contenido dentro de ninguno otro. La utilidad de los directorios es doble: 1. Permiten organizar el sistema de archivos del ordenador de manera que sea más

fácil localizar ficheros en él, pues evitan tener que buscarlos entre todos los ficheros de la máquina y acotar las búsquedas tan sólo a los incluidos en ciertos directorios.

2. Evitan conflictos de nombres, pues si cada aplicación instala sus ficheros en un

directorio propio podrán coexistir en una misma máquina varios ficheros con el

Page 3: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre ficheros y directorios es muy similar a la relación que en C# se establece entre tipos y espacios de nombres.

Como los nombres de ficheros, en .NET los nombres de directorios pueden ser cualquier combinación de caracteres Unicode imprimibles excepto \. Sin embargo, ahora el límite de caracteres que pueden tener está limitado a 248 y se ha reservado un el nombre especial \ para el directorio raíz.

Rutas de ficheros y directorios

Sintaxis en Windows Como cada fichero y directorio está a su vez contenido dentro de otro directorio, para identificarlo unívocamente habrá que indicar el camino que lleva desde el directorio raíz hasta dicho fichero o directorio. Esto se le suele denominar su ruta completa, y en cada sistema operativo sigue su propia sintaxis. Por ahora nos centraremos en la usada en los sistemas Windows, aunque luego veremos el diseño de la BCL es bastante flexible y permite escribir aplicaciones independientes de sistema operativo en este aspecto. En Windows la sintaxis que sigue para escribir una ruta completa es de la forma: <nombreDirectorioPadre>\<nombreFicheroODirectorio> Como cada directorio puede estar contenido dentro de otro directorio, lo que se indica en <nombreDirectorioPadre> es a su vez la ruta completa del directorio donde se encuentra el fichero indicado en <nombreFicheroODirectorio>, y obviamente el primer carácter de cualquier ruta completa será el \ correspondiente al directorio raíz. Por ejemplo, la ruta completa de un fichero datos1.dat incluido dentro de un directorio llamado Datos es \Datos\datos.dat, pero si dicho directorio se encontrase a su vez contenido dentro de Programa entonces sería \Programa\Datos\datos.dat Si al indicar el nombre de un fichero no se diese su ruta completa se consideraría que la ruta especificada es una ruta relativa a la posición actual en el árbol de directorios. Es decir, que se trata de una ruta en la que <nombreDirectorio> ha de considerarse que es el directorio desde el que se la hace referencia al fichero. Por tanto, si desde el directorio \Programa se quisiese hacer referencia al fichero datos.dat del ejemplo anterior bastaría indicar Datos\datos.dat, pero para hacerle referencia desde el directorio \Programa\Datos bastaría indicar datos.dat. Adicionalmente a la sintaxis vista, en sistemas operativos como Windows donde se puede trabajar con múltiples unidades de disco a cada una de las que se les asocia un nombre diferente, la sintaxis anterior para las rutas completas se ve ampliada así: <nombreUnidad>:<nombreDirectorio>\<nombreFichero> Por ejemplo, A:\Programa\Datos\datos.dat haría referencia al fichero datos.dat del directorio Datos incluido dentro del directorio Programa de la raíz del dispositivo de nombre A (unidad de disquete de 3.5”), y C:\Programa\Datos\datos.dat se

Page 4: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

referiría al fichero datos.dat del directorio Datos incluido en el directorio Programa de la unidad C (primera unidad de disco duro) Si no se indicase la sección <nombreUnidad>: se tomaría como unidad del fichero la denominada unidad actual, que no es más que la unidad desde la que se le referencia. Por tanto, \leeme.txt será el fichero leeme.txt de la unidad actual, , y \ será el directorio raíz de dicha unidad. En caso de que se desee especificar un fichero que esté ubicado en una máquina remota, entones para hacerle referencia hay que usar el formato UNC1, cuya sintaxis es: \\<nombreServidor>\<nombreFicheroCompartido> Por ejemplo, \\pc1\Fuentes\principal.cs haría referencia al fichero principal.cs de la carpeta compartida Fuentes ubicada en la máquina pc1.

Rutas independientes del sistema operativo En el epígrafe anterior se explicó el formato de las rutas siguiendo la sintaxis propia de Windows pero se comentó que la BCL incorpora mecanismos que permiten escribir las aplicaciones de manera independiente del formato usado por el sistema operativa de la máquina sobre la que se ejecute el programa. Pues bien, esos mecanismos consisten en escribir dichas rutas usando las siguientes campos char static readonly del tipo Path: Campo Parte de la ruta que su valor representa DirectorySeparatorChar Separador de directorios. En Windows es \, en Unix es / y

Macintosh es : AltDirectorySeparatorChar Carácter alternativo usable como separador de directorios.

En Windows y Macintosh es /, mientras que en Unix es \ PathSeparator Separador entre rutas. Aunque en los sistemas operativos

más comunes es ; podría variar en otros. VolumeSeparatorChar Separador de unidades lógicas. En Windows y Macintosh

es : (por ejemplo c:\datos) y en Unix / Tabla 1: Campos de Path independizadores del formato de rutas de los sistemas operativos

En la versión de la BCL para cada sistema operativo se almacenará en estos campos el carácter correspondiente en dicho sistema a la parte de la ruta que representan. De esta manera, si en lugar de escribir directamente la ruta “\datos\datos.dat” escribimos: String ruta = Path.DirectorySeparatorChar + ”datos” +

Path.DirectorySeparatorChar + ”datos.dat”; Conseguiremos que la variable ruta almacene el formato de la misma según corresponda al sistema operativo sobre el que se ejecute el código anterior. Es decir, mientras que en Windows contendría “\datos\datos.dat”, en Unix contendría “/datos/datos.dat”

1 UNC = Universal Naming Convention o Convenio Universal de Nombrado

Page 5: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Operaciones con rutas Aparte de la utilidad independizadora del sistema opeartivo vista, Path también es muy útil porque incluye un conjunto de miembros estáticos específicamente diseñados para realizar cómodamente las operaciones más frecuentes relacionadas con rutas. La mayoría de estos métodos toman como parámetros cadenas de texto con las rutas sobre la que se desean aplicar la operación que facilitan. Si recuerda, como en Windows el carácter usado como separador de directorios (\) coincide con el que C# usa como indicador de secuencias de escape, en este lenguaje es incorrecto representar rutas como c:\datos con literales como “c:\datos”. En su lugar hay tres alternativas: • Usar el campo independizador del sistema operativo, aunque ello tiene el problema

de que da lugar a códigos poco compacto. Por ejemplo, para la ruta anterior habría que representarla con “c:”+Path.DirectorySeparatorChar+”\datos”.

• Duplicar los caracteres \ de los literales para que dejen de considerarse secuencias de

escape. Así, la ruta de ejemplo anterior quedaría como “c:\\datos” • Especificar la ruta mediante un literal de cadena plano, pues en ellos no se tienen en

cuenta las secuencias de escape. Así, ahora la ruta del ejemplo quedaría como @”c:\datos” Esta será la alternativa que más se usará en el libro ya que para rutas largas se hace muchos más compacta y fácil de leer al no incluir tantos \ duplicados.

Dicho esto, el primer grupo de métodos que veremos son los destinados a facilitarnos la extracción de información sobre las diferentes partes de la ruta que se les pase, que son: • string GetExtension(string ruta): Devuelve la extensión, del fichero o directorio cuya

ruta se le indica incluido el carácter . usado como separador en ella. Así, para una ruta como @”c:\datos\data.dat” devolvá “.dat” Si sólo nos interesa saber si una ruta contiene un fichero o directorio con extensión entonces podemos usar en su lugar el bool HasExtension(string ruta) incluido específicamente para ello.

• string GetFileName(string ruta): Devuelve el nombre del fichero o directorio cuya

ruta se le indica. Por ejemplo, dada la ruta @”c:\datos\data.dat” devolvería “data.dat” • string GetFileNameWithoutExtension(string ruta): Idem al anterior pero sin devolver

la extensión del fichero. O sea, dada la ruta @”c:\datos\data.dat” devolvería “data”. • string GetDirectoryName(string ruta): Devuelve la ruta completa del directorio padre

del fichero o directorio cuya ruta se le indica. Por ejemplo, si dicha ruta es ”c:\datos\data.dat” devolverá c:\datos.

• string GetPathRoot(string ruta): Devuelve la raíz de la ruta indicada. Por ejemplo, si

ésta era @“c:\datos\data.dat” devolverá “c:\”. Si sólo queremos saber si la ruta dispone de raíz podemos usar mejor el bool IsPathRooted(string ruta) incluido para ello.

Aparte de estos métodos también se incluyen otros que permiten hacer tareas de diversos tipo relacionadas con rutas como:

Page 6: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

• Modificar su extensión: El método string ChangeExtension(string ruta, string nuevaExtensión) puede usarse tanto para cambiar la extensión del directorio o fichero cuya ruta se le indica como para añadirle una nueva si no tenía ninguna o quitarle la que tuviese si se le pasa null como nuevaExtensión.

Así, Path.ChangeExtension(@"c:\datos\dato1.dat", “txt”) genera @"c:\datos\dato1.txt", Path.ChangeExtension(@"c:\datos\dato1", “dat”) produce @"c:\datos\dato1.dat", y Path.ChangeExtension(@"c:\datos\dato1.dat", null) devuelve @"c:\datos\dato1"

• Obtenerse su versión completa: El método string GetFullPath(string rutaRelativa) de Path permite obtener la versión completa de la ruta relativa que se le indique. Por ejemplo, si el directorio actual es d:\temp, al llamarle con Path.GetFullPath(“f.txt”) devolverá @“d:\temp\f.txt”

• Concatenar rutas: Dadas dos rutas cualesquiera el método string Combine(string

ruta1, string ruta2) devuelve el resultado de combinarlas siempre. Por ejemplo, dada la llamada Path.Combine(@“c:\datos”, “fichero.txt”) su resulado será “c:\datos\fichero.txt”

Al usar este método tenga cuidado, pues si el resultado de combinar las rutas no da lugar a una ruta válida no se producirá ninguna excepción, sino que en su lugar se devolverá el valor indicado como ruta2. Por ejemplo, el resultado de una llamada como Path.Combine(@“c:\datos”, “@d:\patatas”) es “@d:\patatas”.

Cuando use cualquiera de estos métodos tenga siempre presente una cosa: permiten sólo operar con cadenas de texto que representan rutas pero no están de en ninguna manera relacionados con las ruta que representan. Su utilidad es permitir preparar nuevas rutas a partir de otras ya existentes para luego poder, si se desea, podrán ser pasarse a otros mecanismos para actuar físicamente sobre ellas. Por ejemplo, modificar una ruta con ChangeExtension() no implica que la ruta física en el dispositivo no volátil al que esté asociada quede también modificada, pero la cadena que se devuelva puede usarse luego para modificarla mediante otros métodos que iremos viendo a lo largo del tema.

Representación de ficheros y directorios en .NET

Elementos comunes En la plataforma .NET el trabajo con ficheros y directorios se hace, como no, siguiendo un enfoque orientado a objetos en el que cada se los encapsula dentro de un objetos de ciertos tipos: los ficheros en objetos FileInfo y los directorios en objetos DirectoryInfo. Dado que ambos tipos de elementos dispone de muchas características , lo que se ha hecho en la BCL es definir los tipos envolventes que los representan en base a una clase abstracta común llamada FileSystemInfo que facilite la escritura de código genérico que al sólo usar sus características comunes pueda trabajar tanto con unos como con otros. Según su funcionalidad los miembros comunes ofrecidos por esta clase se agrupan en:

Page 7: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

• Determinación de existencia: Saber si existe físicamente o el fichero o directorio representado por un objeto FileSystemInfo es tan sencillo como mirar el valor de su propiedad bool Exists.

• Borrado: Como tanto los ficheros como los directorios pueden ser borrados, en

FileSystemInfo se proporciona un método Delete() mediante el que es posible borrar ambos tipos de elementos del sistema de archivos. Sin embargo, al usarlo hay que tener en cuenta que si el elemento a borrar es un directorio, éste ha de estar vacío porque si no lanzará una IOException

• Acceso a información: Se ofrecen propiedades de sólo lectura que permiten tanto

obtener cadenas con el nombre completo del fichero o directorio representado (FullName) como con su nombre principal (Name) o su extensión (Extension)

Como muchos sistemas operativos almacenan información sobre las fechas de creación, último acceso y última modificación de los ficheros y directorios de su sistema de archivo, también se proporcionan propiedades (DateTime CreationTime, DateTime LastAccessTime y DateTime LastWriteTime respectivamente) que permiten acceder a ellas tanto para leerlas como para modificarlas.

Sin embargo, no todos los sistemas operativos almacenan esta información o si lo hacen permiten modificarla. Si un sistema operativo no la admite, la lectura de estas propiedades devolverá null; y si las admite pero no permite modificarlas (como es el caso de Windows 95 y Windows 98 con los directorios), las escrituras en ellas no tendrán ningún efecto y serán completamente ignoradas.

• Por otro lado, los sistemas operativos también suelen asociar una serie de atributos a cada uno de sus ficheros y directorios que describen cómo ha de interpretarse bajo determinadas circunstancias. Mediante la propiedad Attributes podemos acceder a ellos tanto para leerlos como para modificarlos. Esta propiedad es de un tipo enumerado llamado FileAttributes que admite los siguientes literales:

Literal Significa que el fichero o directorio... Normal Es fichero normal y corriente, sin ningún otro atributo Directory Es un directorio, no un fichero Hidden Ha de considerarse como oculto y no mostrarse en los

listados normales del contenido de su directorio padre System Forma parte del sistema operativo ReadOnly Es de sólo lectura Encrypted Está cifrado. Si es un directorio entones los ficheros que

se le añadan se cifrarán automáticamente Compressed Está comprimido Temporary Es temporal y será borrado por el sistema operativo

cuando finalice la ejecución de la aplicación que lo creó Archive Está archivado. Este atributo suelen activarlo las

aplicaciones de backup para marcar los archivos o directorios de los que se dispone de copias de seguridad

Offline No está disponible debido a que está almacenado en una máquina remota con la que no se puede conectar

NotContentIndexed No ha de ser indexado por los servicios de indexado de

Page 8: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

contenidos de los que disponga el sistema operativo SparseFile Es un fichero esparcido, lo que significa que es un fichero

grande cuyo contenido son casi todo ceros. ReparsePoint Contiene un bloque de datos asociado con información

configurable por el usuario Tabla 2: Literales indicadores de atributos de ficheros de FileAttributes

Tenga en cuenta que la tabla anterior abarca atributos propios de muy diversos sistemas operativos que no tienen porqué estar presentes en todos los sistemas operativo. Además, aunque pueda darle la sensación de que cada fichero o directorio sólo puede disponer de sólo uno de los atributos señalados porque su propiedad Attributes devuelve un único objeto, esto no es así porque FileAttributes ha sido definido como una enumeración de flags y sus objetos pueden almacenar combinaciones OR de sus literales. Por ejemplo, un método que convierta ficheros o directorios en ocultos y de sólo lectura puede escribirse así:

public void ConvierteEnOcultoDeSoloLectura(FileSystemInfo f) { f.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; }

Dado que los atributos asociados a un fichero pueden ir cambiando dinámicamente, es importante que siempre que se acceda a ellos se obtenga una versión actualizada de los mismos. Para conseguirlo basta llamar al método Refresh() del objeto FileSystem que lo representa. A modo de resumen a continuación se muestra un ejemplo de método que hace uso de todas estas propiedades para mostrar por la consola la información asociada al fichero o directorio que representa el objeto FileSystemInfo que se le indique:

public static void muestraInfo (FileSystemInfo f) { if (f.Exists)

{ Console.WriteLine("Nombre completo: {0}", f.FullName);

Console.WriteLine("Nombre : {0}", f.Name); Console.WriteLine("Extensión : {0}", f.Extension); Console.WriteLine("Fecha creación: {0}", f.CreationTime);

Console.WriteLine("Fecha último acceso: {0}", f.LastAccessTime); Console.WriteLine("Fecha última modificación: {0}", f.LastWriteTime);

Console.WriteLine("Atributos: {0}", f.Attributes.ToString("F")); } else throw new FileNotFoundException();

}

Aunque el trabajo con ficheros y con directorios es muy similar –de hecho, como ya hemos visto, los tipos que los representan derivan de una clase común FileSystemInfo-, también es cierto que cada uno de ellos dispone tiene una serie de características propias que lo diferencian del otro. Por esta razón, en la BCL se ha definido como abstracta FileSystemInfo y se han incluido subclases suyas no abstractas FileInfo y DirectoryInfo que añaden a los miembros comunes heredados de su padre miembros relacionados con funcionalidades particulares de cada tipo de elemento.

Page 9: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Elementos específicos de directorios Para crear el objeto DirectoryInfo que represente a un determinado directorio basta pasar como parámetro de su constructor una cadena con la ruta –relativa o completa- del mismo. Por ejemplo, un objeto d que represente al directorio c:\Pruebas se crea así: DirectoryInfo d = new DirectoryInfo(@”c:\Pruebas”); Este directorio no tiene porqué existir, pues como se verá más adelante es posible crear directorios a través de objetos DirectoryInfo que representen directorios no existentes. Como ya se ha dicho, este objeto es en realidad de una subclase de FileSystemInfo y por tanto dispondrá de todos los miembros definidos en ella además de los explícitamente definidos en DirectoryInfo. Estos miembros adicionales le aportan las funcionalidades específicas del trabajo con directorios descritas a continuación.

Información adicional sobre el directorio A la información sobre un directorio que proporciona FileSystemInfo (nombre, fechas, atributos, etc.), DirectoryInfo añade los siguientes datos: • Directorio padre: Aunque a partir del nombre completo de un directorio que la

propiedad FullName de FileSystemInfo proporciona puede deducirse con facilidad cuál es su directorio padre, DirectoryInfo proporciona una propiedad DirectoryInfo Parent {get;} que permite consultar a esta información con mayor comodidad.

Obviamente no tiene mucho sentido aplicar esta propiedad a un DirectoryInfo que represente al directorio raíz ya que éste carece de padre, y hacerlo devuelve null.

• Directorio raíz: Si en lugar del padre inmediato de un directorio lo que se desea es

obtener su ancestro más antiguo (directorio raíz) puede consultarse cuál es éste directamente a través de su propiedad DirectoryInfo Root {get;}

Acceso al contenido de un directorio Los objetos DirectoryInfo cuentan con métodos con los que pueden obtenerse tablas con todos los ficheros (FileInfo[] GetFiles()), directorios (DirectoryInfo[] GetDirectories()), o ficheros y directorios (FileSystemInfo[] GetFileSystemInfos()) contenidos en el directorio que representan. Por ejemplo, todos los ficheros de c:\Windows pueden obtenerse así: FileInfo[] ficheros = new DirectoryInfo(@”C:\Windows”).GetFiles(); Cada uno de estos métodos tiene una sobrecarga que admite como parámetro una cadena con la que pueden filtrarse los nombres de los ficheros o directorios que se desean obtener. Dicho filtrado consiste en usar los caracteres ? y * como comodines con los que representar, respectivamente, cualquier carácter y cualquier número de

Page 10: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

cualesquiera caracteres. Así por ejemplo, si en vez de todos los ficheros de c:\windows queremos obtener tan sólo los ejecutables (extensión .exe) podemos hacer lo siguiente: FileInfo[] ficheros = new DirectoryInfo(@”C:\Windows”).GetFiles(*.exe);

Operaciones típicas de manipulación de directorios Todo sistema operativo proporciona mecanismos mediante los que sus usuarios pueden realizar modificar la estructura de directorios del sistemas de archivos de las máquinas, realizando tareas tales como crear nuevos directorios, eliminar directorios existentes, etc. Pues bien, para poder realizar este tipo de tareas de una manera automatizada desde nuestro programas, la clase DirectoryInfo incluye el siguiente conjunto de métodos que permiten realizarlas programáticamente: • Creación de directorios: Para crear físicamente el directorio representado por un

objeto DirectoryInfo basta aplicarle al mismo su método Create(). Por ejemplo, para crear un subdirectorio Test dentro c:\Windows se puede hacer lo siguiente:

new DirectoryInfo(@“c:\Windows\Test”).Create();

Si el directorio a crear de esta forma ya existe no se producirá ninguna excepción. Simplemente la ejecución de Create() no tendrá absolutamente ningún efecto.

• Delete(): Borra el directorio vacío representado por el objeto DirectoryInfo sobre el

que se aplica. Si ese directorio no está vacío entonces hay que pasarle true como parámetro para indicar explícitamente que deseamos también borrar su contenido, pues como mecanismo de seguridad si no lo hiciésemos se lanzaría una IOException

• MoveTo(string directorioDestino): Mueve el directorio y todos sus contenidos al

nuevo directorio que se le indica. Por ejemplo, para trasaladar el contenido de un directorio c:\Datos1 en otro directorio c:\Datos2 bastaría hacer:

DirectoryInfo d = new DirectoryInfo(@“c:\Datos1”); d.MoveTo(@“c:\Datos2”);

El directorio de destino especificado no debe existir, pues si no se lanzaría una IOException. Si lo que se desea es trasladar el contenido de un directorio a otro ya existente entonces hay que especificar en la llamada a MoveTo() el nombre que se desea que tenga en el directorio de destino. Es decir, si queremos copiar c:\Datos1 dentro de un directorio c:\Datos2 ya existente entonces habrá que hacer:

DirectoryInfo d = new DirectoryInfo(@“c:\Datos1”); d.MoveTo(@“c:\Datos2\Datos1”);

Elementos específicos de ficheros Del mismo modo que para representar específicamente directorios se proporcionan objetos DirectoryInfo, para representar ficheros se proporcionan objetos FileInfo Este tipo también desciende de FileSystemInfo y se utiliza de forma similar a los objetos

Page 11: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

DirectoryInfo ya vistos. Así por ejemplo, para crear un objeto f de tipo FileInfo que represente a un fichero c:\winnt\wplog.txt se puede hacer: FileInfo f = new FileInfo(@”c:\winnt\wplog.txt”); A través de este objeto se pueden hacer las operaciones típicas de creación (Create()), borrado (Delete()) y movimiento (MoveTo()) de ficheros de manera análoga a como se ha visto que se hacen con los directorios. Adicionalmente se ha añadido un método CopyTo() que funciona de manera similar a MoveTo() sólo que en vez de cambiar al ubicación del fichero lo que hace es crear una copia del mismo en la ubicación indicada. De nuevo, para evitar sobreescrituras no deseadas ambos métodos no admiten que el fichero de destino especificado ya exista lanzan IOExceptions si fuese así, pero ello también puede cambiarse pasándoles true como segundo parámetro. En lo referente al acceso a información adicional sobre el fichero hay que señalar que en este caso la analogía con el caso de los directorios no es tan pura, pues para obtener el directorio padre de un fichero la propiedad a usar del objeto FileInfo que lo representa no se llama ahora Parent sino DirectoryName. Además, también se ofrece una segunda propiedad llamada Directory que permite acceder a esa misma información pero en forma de objeto DirectoryInfo en lugar de cadena de texto. Como habrá adivinado, aparte de estos miembros los objetos FileInfo deben también de ofrecer mecanismos que permitan acceder al contenido de los ficheros que representan, ya sea para leerlo o para modificarlo. Pues bien, precisamente a explicar cómo se realiza eso es a lo que esta destinado los siguientes epígrafes.

Flujos de entrada-salida La forma con la que se trabaja con ficheros en la plataforma .NET está íntimamente ligada al concepto de flujo de entrada-salida, que consiste en tratar su contenido como si de una secuencia ordenada de bytes se tratase. Este concepto no está ligado en exclusividad a los ficheros, sino que es un concepto abstracto aplicable a otros tipos de almacenes de información tales como conexiones de red o buffers en memoria. En realidad se distinguen dos tipos de flujos: • Flujos base: Trabajan directamente con algún tipo de medio físico, como puede ser

una porción de memoria, de espacio en disco o una conexión a red. • Flujos intermedios: No trabajan con medios físicos directamente sino envuelven a

otros flujos a los que proporcionan diferentes características, pudiéndose combinar entre sí de manera que el flujo base que haya tras ellos pueda verse beneficiado por todas las funcionalidades que ofrezcan todas ellos. Ejemplos son los flujos que proporcionan encriptación de datos o buffers de almacenamiento intermedio.

En la BCL todos los tipos relativos al trabajo con flujos se encuentran agrupados dentro del espacio de nombres System.IO Un tipo muy importante entre ellos es Stream, que es la clase abstracta base de todos los flujos y les proporciona los miembros básicos que permiten trabajar con ellos. Estos miembros se explicarán en los epígrafes que siguen.

Page 12: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Lectura y escritura Como es lógico, todo flujo dispondrá de mecanismos mediante los que se le puedan extraer y añadir bytes. No todos los flujos tienen porqué admitir ambas operaciones, por lo que para saber qué operaciones admite cada flujo concreto Stream añade a todos ellos dos propiedades bool CanRead y bool CanWrite que permite consultarlo. Para la lectura se ofrecen métodos ReadByte() y Read() que, respectivamente, permiten extraerles uno o varios bytes (se almacenarían en una tabla byte[]); y para la escritura se hace lo mismo con WriteByte() y Write(). Obviamente los métodos de lectura sólo serán aplicables flujos que admitan lectura y los de escritura sólo serán aplicables a los que admitan escritura, y si se intentan aplicar a flujos que no los admitan se lanzarán excepciones NotSupportedException.

Los métodos anteriores funcionan síncronamente, lo que significa que tras llamarlos el código que los llamó se quedará en espera de que terminen de ejecutarse. Sin embargo, como las operaciones de entrada-salida suelen ser lentas también se ofrecen parejas de métodos BeginRead()-EndRead() y BeginWrite()-EndWrite() que permiten realizarlas asíncronamente de manera que mientras se estén realizando el código llamador pueda seguir ejecutándose y realizando otras tareas. Estos métodos funcionan análogamente a como lo hacen las parejas de métodos BeginInvoke y EndInvoke de los delegados.

Tras cada lectura la siguiente llamada a estos métodos devolvería los bytes del flujo siguientes a los últimos leídos, y para detectar cuando se alcance el final del flujo basta mirar su valor de retorno, pues en ese caso ReadByte() devolverá un –1 y Read() un 0. Respecto a la escritura, cada vez que se escriba se escribirá a continuación de los últimos bytes escritos en el flujo.

Movimiento por el flujo Por defecto, la primera operación que se realice sobre un flujo se aplica a su comienzo y las siguientes se aplican tras la posición resultante de la anterior. Es decir, en cada lectura o escritura se lee o escribe a continuación de la última posición accedida, por lo que antes de acceder a una determinada posición habrá que pasar antes por las previas. A esto se le conoce como acceso secuencial.

Sin embargo, hay situaciones en las que puede resultar interesante poderse escribir o leer de cualquier posición del flujo sin necesariamente tener que pasar antes por sus anteriores. A esto se le conoce como acceso aleatorio, y como no todos los flujos tienen porqué admitirlo, cada flujo dispone de una propiedad de sólo lectura bool CanSeek que indica si lo admite.

Si un flujo admite acceso aleatorio le serán aplicables los miembros de Stream relativos a dicho tipo de acceso que a continuación se muestran, mientras que si no lo admite la utilización de los mismos provocará excepciones NotSupportedException:

• long Length {get;}: Número de bytes almacenados en el flujo (tamaño del flujo) • SetLength(long tamaño): Cambia el número de bytes almacenados en el flujo por

el indicado. Si este nuevo tamaño es inferior al que tenía se truncarán los bytes

Page 13: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

que no sobren, mientras que si es superior se rellenarán los nuevos bytes con valores inespecificados y conviene darles a mano valores por defecto.

A diferencia de Length, para poderse ejecutar este miembro no sólo se necesita que el flujo admita acceso secuencial sino que además se necesita que admita la realización de operaciones de escritura.

• long Position: Número del byte actual en el flujo. Dándole valores podemos movernos por el flujo tal y como muestra el siguiente método de ejemplo:

public void MuestraByte(Stream flujo, long númeroByte) { int posiciónAnterior = flujo.Position;

if (númeroByte > flujo.Length) throw new ArgumentOutOfBoundsException(); flujo.Position = númeroByte; Console.WriteLine(“Byte {0} = {1}”, númeroByte, flujo.ReadByte()); flujo.Position = posiciónAnterior; }

Este método imprime en la consola el byte que se le indique del flujo que se le pase como parámetro. Nótese que, para asegurar que su ejecución sea inocua y no altere el flujo se controla a su comienzo cuál era la posición inicial en el flujo y a su final se le restaura a dicha posición. Por otra parte, también se controla que el númeroByte indicado se encuentre dentro del flujo, pues sino aunque la asignación flujo.Position = númeroByte sería válida y no produciría excepciones, el flujo habría quedado en un estado inconsistente y ReadByte() devolvería –1.

• long Seek(long posición, SeekOrigin inicio): Permite colocarnos en un byte del flujo determinado. La posición de éste se indica de manera relativa respecto a la posición de inicio señalada por su segundo parámetro, que es de un tipo de enumeración cuyos posibles literales son Current (posición actual en el flujo), Begin (inicio del flujo) y End (final del flujo) Por ejemplo:

// Coloca justo antes del comienzo del flujo flujo.Seek(0, SeekOrigin.Begin); // Coloca justo después del final del flujo flujo.Seek(0, SeekOrigin.End); // No nos movemos de la posición actual flujo.Seek(0, SeekOrigin.Current); // Coloca dos bytes a continuación de la posición actual en el flujo flujo.Seek(2, SeekOrigin.Current); // Coloca dos bytes antes de la posición actual en el flujo flujo.Seek(-2, SeekOrigin.Current); // Coloca en el segundo byte del flujo flujo.Seek(2, SeekOrigin.Begin); // Coloca en el penúltimo byte del flujo flujo.Seek(-2, SeekOrigin.End);

Page 14: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Volcado de datos en flujos Muchos flujos trabajan internamente con buffers donde se almacenan temporalmente los bytes que se solicitan escribir en ellos hasta que su número alcance una cierta cantidad, momento en que son verdaderamente escritos todos a la vez en el flujo. Esto se hace porque las escrituras en flujos suelen ser operaciones lentas e interesa que se hagan el menor número de veces posible.

Sin embargo, hay ocasiones en puede interesar asegurarse de que en un cierto instante se haya realizado el volcado físico de los bytes en un flujo, ya sea porque de ello dependa el correcto funcionamiento de otras partes de la aplicación o se sepa que no se volverá a escribir en él hasta que dentro de mucho. En esos casos puede forzarse el volcado llamando al método Flush() del flujo, que vacía por completo su buffer interno.

Cierre de flujos Además de memoria, muchos flujos acaparan recursos extra que interesa liberar una vez dejen de ser útiles. Por ejemplo, las conexiones de red acaparan sockets y los ficheros acaparan manejadores de ficheros del sistema operativo, que son recursos limitados y si no liberarlos podría impedir la apertura de nuevos ficheros o conexiones de red. Como podrá adivinar, en principio ello es tarea del método Dispose() con el que todo flujo cuenta como resultado de implementar la interfaz IDisposable que se usa en .NET como estándar recomendado con el que liberar recursos de manera determinista. Sin embargo, por similitud con las APIs de manejo de flujos de otros lenguajes a las que muchos programadores estarán acostumbrados, en .NET la clase Stream también dispone de un método Close() que hace lo mismo. En cualquier caso, lo que es importante saber es que una vez cerrado el flujo –ya sea llamando a Dispose() o a Close()- ya no es posible hacer uso del mismo y cualquier intento de acceder a sus miembros provocará excepciones ObjectDisposedException

Flujos de entrada-salida en ficheros El contenido de los ficheros se trata en la plataforma .NET como flujos de entrada-salida, por lo que para manipularlo se usarán los métodos de Stream ya vistos. Sin embargo, si recuerda dicha clase es abstracta y por tanto es imposible crear objetos de ella. Como habrá adivinado, lo que en realidad se hace es usar una subclase suya especializada en el trabajo con flujos asociados a ficheros. Ésta clase es FileStream.

Creación de objetos FileStream Creación a partir de objetos FileInfo A crear el FileStream que permita manipular el contenido de un fichero se le denomina abrir el fichero, y una forma sencilla hacerlo es mediante el método Open(FileMode

Page 15: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

modoApertura) del objeto FileInfo que lo representa. Su parámetro indica qué ha de hacerse si el fichero a abrir ya existe, y es de un tipo enumerado cuyos literales son: Literal de FileMode Modo de apertura Open Abre el fichero presuponiendo existe. Si no fuese así se lanza una

FileNotFoundException.

OpenOrCreate Si el fichero no existe, lo crea CreateNew Abre el fichero presuponiendo que no existe y creándolo. Si

existiese se lanzaría una IOException Create Crea el fichero, y si ya existía lo sobreescribe Truncate Abre el fichero presuponiendo que existe y borrando todo su

contenido. Si no existiese se lanzaría una FileNotFoundException Append Abre el fichero en modo de concatenación, lo que significa que

sólo podrá escribirse a su final. Si el fichero no existe, lo crea. Tabla 3: Modos de apertura de ficheros (enumeración FileMode)

Por ejemplo, para abrir un fichero preexistente llamado c:\datos.dat puede hacerse: FileInfo fichero = new FileInfo(@“c:\datos.dat”); FileStream contenidoFichero= fichero.Open(FileMode.Open); Además del modo de apertura, también puede resultar interesante especificar qué es lo que deseamos hacer con el contenido del fichero: si sólo leerlo, si sólo escribir en él o si ambas cosas; pues muchos sistemas operativos permiten asociar a los ficheros permisos de acceso con los que puede restringirse qué puede hacerse con cada uno. Como si no indicamos nada se considerará que queremos tanto leer como escribir en el fichero, y para especificar cualquier otra cosa basta pasar a Open() un parámetro adicional del tipo enumerado FileAccess que indique como se desea abrirlo. Los literales de este tipo son:

Literal de FileAccess Modo de acceso solicitado Read Sólo lectura del contenido Write Sólo escritura de contenido ReadWrite Tanto lectura como escritura de contenido

Tabla 4: Modos de acceso a ficheros (enumeración FileAccess)

Fíjese que algunos modos de aperturas de ficheros, por su propia definición, requieren de ciertos permisos de acceso. Por ejemplo, un fichero sólo puede abrirse en modo de concatenación (FileMode.Append) si se tiene permiso de acceso FileAccess.Write; y ni siquiera se permite abrirlo si se tiene FileAccess.ReadWrite porque es absurdo leer de él. Un ejemplo de cómo abrir un fichero preexistente en modo de sólo lectura es: FileInfo fichero = new FileInfo(@“c:\datos.dat”); FileStream contenidoFichero= fichero.Open(FileMode.Open, FileAccess.Read); Ahora bien, como es bastante frecuente abrir en modo de sólo lectura o de lectura-escritura ficheros preexistentes, a los objetos FileInfo también se les ha dotado de un par de métodos FileStream OpenRead() y FileStream OpenWrite() que permiten, de manera respectiva, abrir así los ficheros que representan. Por tanto, en el ejemplo anterior en realidad podría haberse escrito de la siguiente manera mucho más compacta:

Page 16: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

FileInfo fichero = new FileInfo(@“c:\datos.dat”); FileStream contenidoFichero= fichero.OpenRead(); Por otra parte, la mayoría de los sistemas operativos permiten controlar la forma en que diferentes procesos puede acceder simultáneamente a sus ficheros. Por seguridad, para evitar problemas de concurrencia no se permite que múltiples hilos puedan compartir ficheros abiertos con cualquiera de las sobrecargas de Open() vistas. Sin embargo, ello puede cambiarse especificando un tercer parámetro de tipo enumerado FileShare, cuyos posibles literales son:

Literal de FileShare Modo de compartición None No se puede compartir Read Puede compartirse con hilos que sólo quieran leerlo Write Puede compartirse con hilos que sólo quieran escribir en élReadWrite Puede compartirse con cualquier otro hilo

Tabla 5: Modos de compartición de ficheros (enumeración FileShare)

Creación con el constructor de FileStream Hasta ahora todas las formas de crear objetos FileStream que hemos visto eran métodos proporcionados por la clase FileInfo que encapsula ficheros, lo que nos obliga a siempre tener que crear un objeto de este tipo para poder acceder al contenido de un fichero. Como ello puede resultar ineficiente, la propia clase FileStream también proporciona una familia de constructores que permiten crear objetos suyos mucho más directamente. Así, los siguientes constructores son equivalentes a las tres sobrecargas de Open() vistas:

FileStream(string rutaFichero, FileMode modoApertura) FileStream(string rutaFichero, FileMode modoApertura, FileAccess modoAcceso) FileStream(string rutaFichero, FileMode modoApertura, FileAccess modoAcceso,

FileShare modoCompartición) Por ejemplo, para leer el contenido del fichero c:\datos.dat podemos generar un FileStream así: FileStream contenidoFichero = new FileStream(@”c:\datos.dat”, FileMode.Open,

FileAccess.Read);

Manipulación del contenido de los ficheros Como ya se ha repetido hasta la saciedad, el contenido de los ficheros es tratado en la plataforma .NET como flujos de entrada-salida representados por objetos FileStream; y dado que esta clase deriva de Stream (clase padre de todos los flujos), para manipularlo podemos usar cualquiera de los miembros ya vistos para ésta última. En principio los ficheros admiten tanto lectura como escritura y movimiento por su contenido, por lo que las propiedades CanRead, CanWrite y CanSeek de los objetos FileStream valdrán siempre true y los métodos ReadByte(), Read(), WriteByte(), Write() y Seek() heredadas de Stream serán plenamente utilizables. Sin embargo, como es obvio, esto dependerá de cómo hayamos abierto el fichero, pues si por ejemplo lo abrimos en

Page 17: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

modo sólo lectura, CanWrite devolverá false y métodos como WriteByte() o Write() dejarán de funcionar y provocarán NotSupportedExceptions. Dada la relativa lentitud de las operaciones de acceso a ficheros, para mayor eficiencia durante su realización se ha optado por asociar un buffer a cada FileStream en el que los bytes que se soliciten escribir se irán almacenando hasta alcanzar un cierto número para entonces escribirlos todos de una vez. Por defecto el tamaño de este buffer es de 8192 bytes, pero puede cambiarse usando el siguiente constructor para crear el FileStream:

FileStream(string rutaFichero, FileMode modoApertura, FileAccess modoAcceso, FileShare modoCompartición, int tamañoBuffer)

Al especificar así un tamaño de buffer hay que tener en cuenta que no conviene darle un tamaño inferior a 8 bytes, pues sería tan pequeño que prácticamente no se ganaría nada de eficiencia. Es más, cualquier valor inferior a dicha cantidad que se le dé será ignorado y se tomará en su lugar un tamaño de 8 bytes. Lo que si es importante recordar es que debido a este buffer, cuando queramos estar seguros de que en un momento concreto se haya volcado todo su contenido en el fichero al que está asociado, hemos de llamar al método Flush() Por cierto, por si lo estaba pensando: cuando hayamos terminado de usar un FileStream y vayamos a cerrarlo no tenemos porqué preocuparnos de vaciar este buffer, pues el método Close() usado para ello se encarga automáticamente de hacerlo. Nótese que hasta ahora no se ha dicho nada acerca de las versiones asíncronas de los métodos de lectura-escritura heredados de Stream. Esto se debe a que por defecto el acceso al contenido de los ficheros se realiza siempre de manera síncrona, aunque se usen los métodos anteriores. Esto se debe a que para ficheros pequeños es la forma de acceso más eficiente, que son los más frecuentemente utilizados. Sin embargo, como al trabajar con ficheros grandes puede resultar más eficiente leerlos asíncronamente para poder realizar otras tareas mientras tanto, también se permite activar el acceso asíncrono. Para ello basta crear el FileStream usando el siguiente constructor:

FileStream(string rutaFichero, FileMode modoApertura, FileAccess modoAcceso, FileShare modoCompartición, int tamañoBuffer, bool asíncrono?)

Aunque el parámetro asíncrono? de este constructor indica si se desea habilitar el acceso asíncrono al contenido del fichero a través de las parejas de métodos BeginWrite()-EndWrite(), BeginRead-EndRead(), en realidad nada asegura que vaya a realizarse así puesto que no todos los sistemas operativos soportan dicho tipo de acceso (por ejemplo, Windows 9X y Windows ME) y en los que no lo hagan seguirán funcionando de forma síncrona. Además, si el acceso que se realiza es demasiado pequeño (menos de 64 KB en Windows) también puede ocurrir que el sistema operativo decida que es más eficiente realizarlo de manera síncrona. Por estas razones, para poder consultar cómo se pueden realizar los accesos a cada FileStream éstos disponen de una propiedad llamada bool isAsync {get;} que indica si se les puede acceder asíncronamente.

Aplicación de ejemplo de acceso a ficheros: visor hexadecimal

Page 18: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

A modo de ejemplo que sintetice todo lo visto hasta ahora respecto al acceso a ficheros y su contenido vamos a desarrollar una sencilla aplicación de consola que actúe como un visor hexadecimal que muestre toda la información relativa al fichero que se le pase como argumento al llamarla así como los bytes que contiene. Su código es el siguiente: using System; using System.IO; public class VisorHexadecimal { static void Main(string[] args) { if (args.Length < 1) { Console.WriteLine("Error: Llamada incorrecta. Formato correcto de uso:"); Console.WriteLine("\n\t VisorHexadecimal <rutaFichero>"); Environment.Exit(1); } FileInfo fichero = new FileInfo(args[0]); if (!fichero.Exists)

Console.WriteLine("Error: Fichero {0} no encontrado", args[0]); else { muestraInfo(fichero);

muestraContenido(fichero); } } public static void muestraInfo (FileInfo fichero) { Console.WriteLine("Nombre : {0}", fichero.Name); Console.WriteLine("Extensión : {0}", fichero.Extension); Console.WriteLine("Nombre completo: {0}", fichero.FullName); Console.WriteLine("Directorio padre: {0}", fichero.DirectoryName); Console.WriteLine("Fecha creación: {0}", fichero.CreationTime); Console.WriteLine("Fecha último acceso: {0}", fichero.LastAccessTime); Console.WriteLine("Fecha última modificación: {0}", fichero.LastWriteTime); Console.WriteLine("Atributos: {0}", fichero.Attributes.ToString("F")); Console.WriteLine("Tamaño: {0}", fichero.Length); } public static void muestraContenido(FileInfo fichero) { using (FileStream contenido = fichero.OpenRead()) { if (fichero.Length==0) Console.WriteLine("Sin contenido"); else { byte[] bytesLeidos = new byte[16]; long nBytesLeidos = contenido.Read(bytesLeidos, 0, 16); while (nBytesLeidos!=0) { // Mostramos posición en el fichero del primer byte de la línea Console.Write("\n{0:X8}:", contenido.Position-16); // Mostramos línea de bytes for (int i=0; i<nBytesLeidos; i++)

Page 19: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Console.Write(" {0:X2}", bytesLeidos[i]); // Separamos los bytes de sus respectivos caracteres asegurando que siempre

// se mantenga el mismo espaciado entre ellos for (int i=0; i<16-nBytesLeidos; i++) Console.Write(" "); Console.Write(" | ");

// Mostramos caracteres de los bytes de la línea. Los no imprimibles se // muestran como un carácter de punt (‘.’) for(int i=0; i<nBytesLeidos; i++)

{ char carácterActual = (char) bytesLeidos[i]; if (Char.IsControl(carácterActual)) Console.Write('.'); else Console.Write(carácterActual); } nBytesLeidos = contenido.Read(bytesLeidos, 0, 16); Console.WriteLine(); } } } } } Como se puede ver, el código de esta aplicación es bastante sencillo: simplemente se comprueba que se le pase algún argumento y si así se toma como ruta de un fichero del que se mostrará su información asociada y contenido. La información asociada se muestra a través del método muestraInfo(), que es similar al método de ejemplo sólo que también muestra información específica de los ficheros y no común con los directorios, como el tamaño de su contenido. Para mostrar el contenido del fichero se usa el método muestraContenido(), que lo abre en modo de sólo lectura y lee su contenido byte a byte mostrando por pantalla los bytes así leídos en líneas de 16. Cada una de estas líneas va precedida del número que su primer byte ocupa en el fichero, para así facilitar la determinación de la ubicación en el fichero de cada uno de sus bytes, y seguida de la representación de sus bytes en forma de caracteres, para así facilitar la lectura de ficheros de texto. Por ejemplo, al pasar a este programa como argumento el propio fichero de su código fuente se mostrará: Fíjese que no cierra explícitamente el FileStream tras terminar de usarlo. Esto se debe a que en su lugar se ha usado la novedosa instrucción using de C#, que lo hace automáticamente (en realidad, el cuerpo de Close() es solo una llamada al método Dipose() al que llama using) Sin embargo, usando otros lenguajes sí que podría ser necesario llamar explícitamente a Close()

Control de concurrencia en accesos a ficheros Ya se ha explicado que a través del parámetro de tipo FileShare del constructor de FileStream puede controlarse si se permite el acceso concurrente a un fichero en función del tipo de operaciones que quieran realizarse con él.

Page 20: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Sin embargo, este mecanismo controla de manera global el acceso al fichero y no es muy eficiente cuando se conoce de antemano qué trozos concretos de cada fichero va a necesitar cada proceso. Por ello, para estos casos se ha incluido en FileStream la siguiente pareja de métodos que ofrecen la posibilidad de que cada hilo pueda bloquear temporalmente el acceso a determinadas secciones de los ficheros: • Lock(long byteInicial, long nBytes): Bloquea el acceso a la sección del fichero que

comienza en el byteInicial indicado y cuyo tamaño en bytes es nBytes • Unlock(long byteInicial, long nBytes): Desbloquea el acceso a la sección del fichero

indicada. Si dicha sección no estaba bloqueada no se preocupe, pues no pasará nada.

Acceso nativo a ficheros Muchos sistemas operativos usan internamente manejadores de archivos para trabajar con los ficheros, y los FileStream de la plataforma .NET no hacen más que encapsular el uso de éstos ofreciendo una visión orientada objetos y más cómoda de los mismos. Sin embargo, este nivel de indirección adicional implica también que la manipulación de los ficheros será más ineficiente que si se usasen directamente los manejadores de archivos. Para los casos en que la eficiencia en los accesos a archivos sea crítica, .NET también se da la posibilidad de trabajar directamente con sus manejadores asociados y podérselos pasar como parámetros a métodos nativos que trabajen con ellos más eficientemente. Esto se consigue mediante la propiedad Handle de los objetos FileStream, que devuelve un objeto System.IntPtr que representa al manejador que encapsula. Hay que tener en cuenta que los manejadores así devueltos pueden ser utilizados por ciertas funciones nativas del sistema operativo, como las funciones ReadFile() y WriteFile() de la API Win32, pero ello no significa que también puedan usarse con funciones que esperen descriptores de ficheros tales como la fread() de la librería de C. Recíprocamente, a partir de un manejador de archivo también es posible generar un FileStream que lo encapsule. Para ello, basta usar cualquiera de las sobrecargas de los constructores de FileStream que se muestran a continuación: FileStream(IntPtr manejador, FileAccess modoAcceso) FileStream(IntPtr manejador, FileAccess modoAcceso, bool dueño?) FileStream(IntPtr manejador, FileAccess modoAcceso, bool dueño?, int tamañoBuffer) FileStream(IntPtr manejador, FileAccess modoAcceso, bool dueño?, int tamañoBuffer,

bool asíncrono?) Aunque la mayoría de los parámetros de estos constructores ya los conoce, dueño? es nuevo e indica si se ha de considerar que el FileStream que se cree es el dueño del manejador y por tanto su cierre ha de implicar el cierre de éste también. Por defecto el primer constructor considera que lo es, y si no desea ocurra esto basta con que utilice cualquiera de los demás constructores dándole el valor false a dicho parámetro.

Fichero binarios

Page 21: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

La forma de acceder a ficheros que hasta ahora hemos visto tiene una seria limitación, y es que trabaja con la información a nivel de bytes, lo cual no suele ser muy conveniente en tanto que generalmente en las aplicaciones no suele trabajarse directamente con bytes sino con objetos más complejos formado por múltiples bytes. Por ejemplo, dado que un int ocupa 4 bytes, para almacenar su valor en un fichero usando un FileStream habría que realizar una operación WriteByte() por cada uno de sus bytes o descomponer todos los en una tabla byte[] y escribirlos de una vez con una llamada a Write() Como esta no es ni por asomo una solución cómoda, en System.IO se han incluido un par de tipos llamados BinaryReader y BinaryWriter que encapsulan FileStreams y les proporcionan una serie de métodos con los que se simplifica la lectura y escritura de objetos de cualquier tipo en ficheros. Aunque similares a flujos intermedios, no hay que confundir estos tipos con tales, pues aunque encapsulan a otros flujos y disponen de muchos de los métodos comunes a éstos, no derivan de Stream. Para crear objetos de estos tipos simplemente basta con pasar como parámetro de sus respectivos constructores el flujo a envolver, pues éstos son de la forma: BinaryReader(Stream flujo) BinaryWriter(Stream flujo) Una vez encapsulado el flujo siempre es posible extraerlo mediante la propiedad Stream BaseStream {get;} que ambos tipos de objetos proporcionan para ello. Los BinaryWriters se caracterizan por disponer de un método Write() con múltiples sobrecargas que toman parámetros de cualquiera de los tipos básicos excepto object y los escriben directamente en el flujo que encapsulan. Análogamente, los BinaryReaders disponen de una familia de métodos ReadXXX() (int ReadInt(), string ReadString(), etc.) que permiten leer del flujo cualquier tipo básico. Además, adicionalmente se les ha añadido un método int PeekChar() que permite consultar el siguiente carácter del flujo en forma de int (o un –1 si no quedasen más) pero sin extraerlo del mismo, de forma que la siguiente lectura se realizaría como si la consulta nunca hubiese sido realizada. Sin embargo, quizás por error en el diseño de la beta 2, no disponen de un método Seek() análogo al que tienen los BinaryWriters2. Por defecto, se considera que la codificación de las cadenas de texto que se lean o escriban del flujo es UTF-8, pero puede especificarse cualquier otra usando las siguientes sobrecargas de sus respectivos constructores: BinaryReader(Stream flujo, Encoding codificación) BinaryWriter(Stream flujo, Encoding codificación) Como se deduce de los prototipos anteriores, la codificación se indica usando objetos System.Text.Encoding que representarán formatos codificación. Aunque estos objetos permiten acceder a información sobre los diversos formatos de codificación (página de código, bits de preámbulo, etc.) y realizar conversiones entre sus juegos de caracteres, nosotros por ahora sólo los usaremos para indicar el tipo de codificación a usar y los obtendremos de las siguientes propiedades estáticas de Encoding:

2 De todas formas fíjese que puede seguir haciendo accesos aleatorios a través del método Seek() del objeto que encapsula, pues éste puede recuperarse a través de la propiedad BaseStream antes comentada

Page 22: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Propiedad Formato que representa el objeto devuelto ASCII ASCII (7 bits por carácter) Unicode Unicode (16 bits por carácter) usando notación little-endian BigEndianUnicode Unicode (16 bits por carácter) usando notación big-endian UTF8 UTF8 (16 bits por carácter en grupos de 8 bits) UTF7 UTF7(16 bits por carácter en grupos de 7 bits) Default Juego de caracteres usado por defecto en el sistema.

Tabla 6: Propiedades indicadores de tipo de codificación incluidas en Encoding

Por ejemplo, para escribir en un flujo que represente a un fichero llamado datos.dat usando UTF8 al codificar los caracteres puede hacerse lo siguiente: FileStream fichero = new FileStream(“datos.dat”, FileMode.Create); BinaryWriter ficheroBinario = new BinaryWriter(fichero, Encoding.UTF8)); ficheroBinario.Write(“Este mensaje se escribirá en UTF8 dentro de datos.dat”); Del mismo modo, para leer correctamente dicho mensaje habría que hacer algo como: FileStream fichero = new FileStream(“datos.dat”, FileMode.Open); BinaryReader ficheroBinario = new BinaryReader(fichero, Encoding.UTF8)); Console.WriteLine(“Leido de datos.dat: {0}“, ficheroBinario.ReadString()); Lo que es importante es tener en cuenta que la codificación a usar al leer los caracteres de un fichero de texto debería ser siempre la misma que la que se usó para escribirlos en él, pues si no podrían obtenerse resultados extraños al interpretarse mal los datos leídos.

Ficheros de texto Ya se ha visto que a través de los objetos BinaryReader y BinaryWriter pueden leerse o escribirse textos en ficheros mucho más cómodamente que trabajando a nivel de bytes con sus caracteres, como se haría usando directamente FileStreams. Sin embargo, la BCL proporciona otra pareja de tipos llamados TextReader y TextWriter con los que estas tareas se hacen incluso más sencillas.

Lectura de ficheros de texto Para facilitar la lectura de flujos de texto TextReader ofrece una familia de métodos que permiten leer sus caracteres de diferentes formas: • De uno en uno: El método int Read() devuelve el próximo carácter del flujo como, o

un –1 si se ha llegado a su final. Tras cada lectura la posición actual en el flujo se mueve un carácter hacia delante, y por si sólo quisiésemos consultar el carácter actual pero no pasar al siguiente también se ha incluido un método int Peek() que funciona como Read() pero no avanza la posición en el flujo tras la lectura.

• Por grupos: El método int Read(out char[] caracteres, int inicio, int nCaracteres) lee

un grupo de nCaracteres y los almacena a partir de la posición inicio en la tabla que se

Page 23: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

le indica. El valor que devuelve es el número de caracteres que se hayan leído, que puede ser inferior a nCaracteres si el flujo tenía menos caracteres de los indicados o un –1 si se ha llegado al final del flujo. También se incluye un método ReadBlock() que funciona de forma parecida al anterior pero que se queda a la espera de nuevos caracteres si en el flujo no hay disponibles nCaracteres pero se espera que lleguen, lo que puede ocurrir cuando se lee de flujos asociados a una conexiones de red.

• Por líneas: El método string ReadLine() devuelve la cadena de texto correspondiente

a la siguiente línea del flujo o null si se ha llegado a su final. Por compatibilidad con los sistemas operativos más frecuentemente usados, considera que una línea de texto es cualquier secuencia de caracteres terminada en ’\n’, ‘\r’ ó “\r\n”, aunque la cadena que devuelve no incluye dichos caracteres.

• Por completo: Un método muy útil que se ha incluido en la BCL y que las librerías

de muchos otros lenguajes y plataformas no tienen es string ReadToEnd(), que nos devuelve una cadena con todo el texto que hubiese desde la posición actual del flujo sobre el que se aplica hasta el final del mismo (o null si ya estábamos en su final)

Como la mayoría de estos métodos modifican la posición actual en el flujo pueden darse problemas de concurrencia si varios hilos usan a la vez un mismo TextReader. Para evitarlos se ofrece un método estático TextReader Synchronized(TextReader textreader) que devuelve una versión del TextReader que se le pasa como parámetro en la que se asegura la exclusión mutua en el acceso a aquellos miembros suyos que podrían dar problemas al usarlos en entornos concurrentes. Aparte de estos métodos, los TextReaders también incluyen un método Close() mediante el que pueden liberarse los recursos que hubiesen acaparado para poder leer de la fuente que tuviesen asociada. Obviamente, tras llamar a este método no se podrá seguir usando el TextReader para leer de dicha fuente, e intentar de hacerlo provocará IOExceptions. En realidad TextReader es una clase abstracta que define características comunes a objetos capaces de extraer texto de fuentes genéricas. Nosotros para leer ficheros lo que usaremos serán objetos de su subclase StreamReader especializada en la extracción de texto de flujos. Sus constructores básicos son: StreamReader(Stream flujo)

StreamReader(Stream flujo, Encoding codificacion) StreamReader(Stream flujo, bool autodetectarCodificación?) StreamReader(Stream flujo,Encoding codificacion,bool autodetectarCodificación) StreamReader(Stream flujo,Encoding codificacion,bool autodetectarCodificación,

int tamañoBuffer) Como se ve, estos constructores permiten crear el StreamReader a partir del flujo cuyo lectura facilitan (puede luego recuperarse leyendo su propiedad Stream BaseStream {get;}) y que pueden configurarse para usar un cierta codificación y tamaño de buffer La codificación tomada por defecto es UTF-8, aunque puede cambiarse dándole el valor apropiado al parámetro codificación. Además, también se incluye un parámetro llamado autodetectarCodificación que indica si se desea que se deduzca la codificación del texto a partir de los valores de sus primeros bytes. Si no pudiese autodetectarse se consideraría que es la indicada en codificación ó UTF-8 en su defecto. En cualquier caso, siempre es posible saber cuál es la codificación considerada a través de la propiedad Encoding

Page 24: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

CurrentEncoding {get;} del StreamReader, pero al leerla hay que tener en cuenta que su valor puede cambiar tras la primera lectura como consecuencia de una autodetección El tamaño del buffer que internamente usarán los StreamReaders para alojar los caracteres que lean es por defecto de 256 caracteres (4096 bytes) Sin embargo, dicho tamaño puede cambiarse al crearlos indicando su tamaño–en número de caracteres- en el parámetro tamañoBuffer de su constructor. Dicho valor ha de ser de menos 128 (2048 bytes), y si se le diese alguno menor sería ignorado y se tomaría el mínimo señalado. La existencia de un buffer implica que pueda ocurrir que haya momentos en los que la posición actual en el StreamReader sea distinta a la posición actual en su Stream interno debido a que se hayan leído más caracteres de los solicitados con la idea de guardar temporalmente los sobrantes en el buffer por si se solicita en breve su lectura. Además, los StreamReaders suponen que a su flujo interno sólo se accederá a través de ellos, por lo que si se le hiciesen cambios externamente podrían no verlos directamente por estar leyendo de su buffer interno. Para resolver los problemas de inconsistencia que esto podría provocar, todo StreamReader cuenta con un método DiscardBufferedData() que vacía por completo dicho buffer. Como un uso muy frecuente de los StreamReaders es extraer textos de ficheros, también se han definido versiones de cada uno de los constructores anteriormente vistos que en lugar de tomar como primer parámetro un Stream lo que toman es la cadena con la ruta del fichero a leer y a partir de ella generan el Stream apropiado. Éstos son: StreamReader(string rutaFichero)

StreamReader(string rutaFichero, Encoding codificacion) StreamReader(string rutaFichero, bool autodetectarCodificación?) StreamReader(string rutaFichero, Encoding codificacion,

bool autodetectarCodificación?) StreamReader(string rutaFichero, Encoding codificacion,

bool autodetectarCodificación?, int tamañoBuffer)

Aplicación de ejemplo de lectura de ficheros de texto: visor de texto A modo de ejemplo de cómo de sencillo es leer ficheros con un StreamReader, a continuación se muestra el código de un visor de ficheros de texto que muestra todo el texto contenido del fichero cuya ruta se le pasa como argumento: using System; using System.IO; public class VisorTexto { static void Main(string[] args) { try { if (args.Length < 1) { Console.WriteLine("Error: Llamada incorrecta. Formato correcto de uso:"); Console.WriteLine("\n\t VisorTexto <rutaFichero>"); Environment.Exit(1); }

Page 25: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

String nombreFichero = args[0]; StreamReader contenido = new StreamReader(nombreFichero, true); Console.WriteLine("Contenido de {0}:\n{1}", nombreFichero, contenido.ReadToEnd()); } catch (FileNotFoundException) { Console.WriteLine("Error: Fichero {0} no encontrado", args[0]); } } } Nótese cómo ahora la extracción del contenido del fichero es mucho más sencilla que en el caso de trabajar directamente con el FileStream asociado al contenido del fichero, habiéndose reducido el complejo método muestraContenido() del visor hexadecimal de la anterior aplicación de ejemplo a una única llamada a ReadToEnd() en ésta.

Escritura en ficheros de texto Del mismo modo que los TextReaders facilita la lectura de textos procedentes de fuentes genéricas, los TextWriters hace lo propio con la escritura en destinos genéricos. Para ello ofrece métodos que permiten: • Escribir cadenas de texto: Todo TextWriters cuenta con método Write() que permite

escribir cualquier cadena de texto en el destino que tengan asociado. Esta cadena se les pasa como primer parámetro, pero no tiene porqué ser un string sino que puede ser un objeto de cualquier tipo y lo que se haría sería aplicarle su método ToString() para obtener su representación en forma de cadena de texto. También puede pasársele como parámetro una tabla char[], en cuyo caso lo que se escribiría serían todos sus caracteres en el mismo orden en que apareciesen en ella. Si además se usa la sobrecarga Write(char[] caracteres, int posición, int n) se podría delimitar los caracteres de la tabla para sólo escribir los n primeros incluidos a partir de la posición de la tabla indicada. Como pasa con el Write() de Console, también existe sobrecarga de este método que toma como parámetros la cadena a mostrar y un número indefinido de objetos cuya representación como cadenas se escribirá en el destino en el lugar correspondiente a las subcadenas {i} de la cadena que se le pasó como primer parámetro.

• Escribir líneas de texto: Aparte de Write(), todo TextWriter también dispone de un

método WriteLine() que funciona igual que el anterior pero añade en su destino asociado un indicador de fin de línea tras la cadena que se le solicita escribir en él. Dado que el indicador de fin de línea depende de cada sistema operativo, todo TextWriter dispone de una propiedad string NewLine mediante la que podemos configurar cuál queremos que se use al escribir en su destino asociado. Su valor por defecto es el “\r\n” correspondiente al indicador de fin de línea en Windows, pero también puede dársele el valor “\n” correspondiente a Linux3.

3 Aunque puede tomar cualquier otro valor no se recomienda dárselo porque los TextReaders sólo pueden leer adecuadamente las fuentes de texto cuyos indicadores de fin de línea sean “\r” o “\r\n”

Page 26: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Cuando se usen cualquiera de estos métodos hay que tener cuidado con los objetos con valor null que se les pasen, pues no escriben nada en su lugar en el destino del TextWriter pero tampoco lanzan ningún tipo de excepción que informe de ello. Además, también hay que tener cuidado al usarlos en entornos concurrente ya que si varios hilos usan a la vez un mismo TextWriter podrían presentarse problemas de concurrencia. Para evitarlos, a partir del TextWriter puede generarse una versión suya donde se asegure la exclusión mutua al acceder a sus miembros conflictivos llamando para ello a su método estático TextWriter Synchronized(TextWriter textwriter) Aparte de los métodos anteriores, todo TextWriter también incorpora el típico método Close() mediante el que pueden liberarse los recursos que hubiese acaparado para ser capaz de escribir en su destino asociado. Además, como para optimizar dichas escrituras las realizan a través de un buffer intermedio, también cuentan con el método estándar Flush() encargado de vaciarlo para cuando, sin necesidad de cerrar el TextWriter, se necesite asegurar el volcado físico de todo lo escrito a través de él. Como TextReader, la clase TextWriter es en realidad abstracta y nosotros para usar la funcionalidad descrita lo que en realidad usaremos serán objetos de una subclase suya especializada en la escritura en flujos denominada StreamWriter. Sus constructores permiten configurar el flujo donde se escribirá, y la codificación y tamaño de buffer a usar de forma parecida a como se hace en los de StreamReader. Los básicos son: StreamReader(Stream flujo)

StreamReader(Stream flujo, Encoding codificacion) StreamReader(Stream flujo, Encoding codificacion, int tamañoBuffer) Aparte de estos constructores, y como ocurría en los StreamReaders, como es frecuente usar los StreamWriters para escribir en ficheros de texto también se ofrecen versiones de ellos que en lugar de tomar como primer parámetro un Stream toma la ruta del fichero a escribir y a partir de ella generar el Stream apropiado. Éstos son: StreamReader(string rutaFichero) StreamReader(string rutaFichero, bool concatenar)

StreamReader(string rutaFichero, bool concatenar, Encoding codificacion) StreamReader(string rutaFichero, bool concatenar, Encoding codificacion,

int tamañoBuffer) Como se ve, éstos constructores toman un parámetro adicional llamado concatenar. Este parámetro permite indicar qué ha de hacerse si el fichero donde se desea escribir ya existe. Si vale false será sobreescrito, pero si vale true se concatenarán a su final los nuevos datos que se escriban en él. Por defecto vale true. Aparte de los miembros de TextWriter ya explicados, los StreamWriters incorporan una propiedad bool AutoFlush mediante la que es posible conseguir, si se le da el valor true, que todo texto escrito en él usando sus métodos Write() y WriteLine() sea inmediatamente volcado al dispositivo que tiene asociado sin que el programador tenga que acordarse de llamar explícitamente a Flush() tras cada escritura.

Manipulación del sistema de archivos

Page 27: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Ya hemos visto que mediante objetos FileInfo podemos realizar las tareas más comunes de manipulación de archivos, acceso a información sobre ellos y a su contenido; y que mediante objetos DirectoryInfo podemos hacer lo propio con directorios. Sin embargo, la realización de estas tareas con ellos implica la creación de objetos de los tipos citados y ello puede llegar a ser un incordio si sólo tenemos que crearlos para hacer una operación concreta, como por ejemplo renombrar un fichero. Para evitar esto, la BCL incluye una serie de tipos que sólo tiene métodos estáticos y que actúan como utilidades que permiten realizar ese tipo de tareas a partir de las rutas de los ficheros en una sola instrucción, sin tener que andar creando objetos intermedios. El tipo ofrecido para manipular así ficheros es File, mientras que para los directorios es Directory. Hay que tener en cuenta que cada vez que se llame a uno de los métodos de estos tipos se harán las comprobaciones de seguridad necesarias para ver si se tienen los permisos necesarios sobre los archivos que intervengan en la operación. Sin embargo, cuando se usan objetos FileInfo o DirectoryInfo las comprobaciones sólo se hacen sobre el fichero o directorio que ellos representen la primera que se solicite una operación sobre ellos. Por tanto, para a hacer múltiples operaciones sobre un mismo fichero o directorio es más eficiente usar objetos intermedios que los métodos estáticos de los tipos anteriores. Aparte de versiones más cómodas de utilizar de los servicios ofrecidos por los objetos FileInfo y DirectoryInfo, estos tipos también proporcionan nuevos servicios relativos a ficheros y directorios que permiten realizar tareas tan útiles como navegar por el sistema de archivos de una máquina u obtener la lista de unidades lógicas de las que dispone.

Manipulación de ficheros Como ya se ha dicho, el tipo ofrecido en la BCL como utilidad mediante cuyos métodos estáticos se facilita la realización de las operaciones con ficheros más comunes es File. Los servicios ofrecidos por los métodos de este tipo pueden agruparse en: • Acceso a información sobre ficheros: Para manipular directamente la información

que los sistemas operativos almacenan sobre los ficheros de su sistema de archivo (atributos, fecha de creación, etc) se ofrecen métodos que operan de manera similar a las propiedades que para ello se vio que ofrecían los objetos FileInfo. Éstos son:

FileAttributes getAttributes(string rutaFichero) SetAttributes(string rutaFichero, FileAttributes atributos) DateTime getCreationTime(string rutaFichero) SetCreationTime(string rutaFichero, DateTime fechaCreación) DateTime getLastAccessTime(string rutaFichero) SetCreationTime(string rutaFichero, DateTime FechaÚltimoAcceso) DateTime getLastWriteTime(string rutaFichero) SetWriteTime(string rutaFichero, DateTime FechaÚltimaModificación)

• Acceso al contenido de los ficheros: Para acceder al contenido de los ficheros se

ofrece un método sobrecargado Open() que funciona de igual forma a como lo hace el Open() de FileStream ya visto pero que toma como primer parámetro su ruta.

Page 28: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Aparte del anterior, también se incluyen métodos FileStream OpenRead(string rutaFichero) y FileStream OpenWrite(stringrutaFichero) que permiten abrir ficheros ya existentes en exclusividad y modo de sólo lectura o de lectura-escritura según, respectivamente, corresponda. También se ofrecen un método StreamReader OpenText(string rutaFichero) que funciona de forma parecida a OpenRead() pero en lugar de devolver directamente el flujo asociado al fichero devuelve un StreamReader facilita su lectura como texto. Aunque de OpenWrite() no se ofrece un equivalente tan fiel para facilitar la escritura de texto en el fichero, se incluye un StreamWriter AppendText(string rutaFichero) que no requiere que el fichero a abrir exista (si no existe lo crea) y que si existe, en vez de sobreescribirlo lo que hace es concatenar a su final el texto que se le escriba.

• Modificación de los ficheros del sistema de archivos: Finalmente, se incluye un tercer grupo de métodos que permiten realizar de una manera cómoda operaciones tan frecuentes como crear, borrar o mover ficheros por el sistema de archivos.

Para crear ficheros se incluye el método FileStream Create(string rutaFichero), que nótese que devuelve un FileStream. Este objeto lo devuelve porque tras crearlo lo más normal es que interese añadirle datos a través de él. Además, se ofrece una sobrecarga del mismo que toma como segundo parámetro un int que se interpretará como el tamaño que ha de tener el buffer que dicho FileStream usará internamente. Antes crear un fichero puede que nos interese ver si previamente ya existía, pues si fuese así se borraría el existente. Para ello puede usarse el método estático bool Exists(string rutaFichero) también incorporado en File. Si lo que se desea es trasladar la ubicación de un fichero en el sistema de archivos entonces el método a usar es Move(string rutaFuente, string rutaDestino) Nótese que este método también puede usarse para cambiar el nombre del fichero, pues ese sería el efecto que se conseguiría si el directorio que se especificase rutaDestino fuese el mismo que el directorio donde estaba el fichero. Si en vez de cambiarlo de ubicación lo que queremos es crear una copia suya en otra ruta, entonces el método a usar es Copy(string rutaFuente, string rutaCopia) Cuando se use hay que tener cuidado con que el fichero rutaCopia no exista, pues para evitar sobreescrituras no deseadas se lanzaría una IOException en caso de que existiese. Otra posibilidad sería pasarle true como tercer parámetro a Copy() para así indicar explícitamente que deseamos sobreescribirlo. Finalmente, para borrar ficheros se ofrece un método Delete(string rutaFichero)

Manipulación de directorios Como también ya se ha comentado, la clase Directory centraliza todo tipo de servicios relativos a la manipulación de directorios y permite utilizarlos de una manera rápida y cómoda, sin necesidad de tener que andar creando objetos DirectoryInfo intermedios. En general, estos servicios que ofrece podemos agruparlos en tres grandes categorías:

Page 29: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

• Acceso a información sobre directorios: Igual que, como se ha visto, File cuenta

con miembros que permiten acceder a la información que cada sistema operativo almacenan sobre los ficheros de su sistema de archivos, Directory hace lo propio con la relativa a los directorios. Para ello, ofrece los siguientes métodos:

DateTime getCreationTime(string rutaDirectorio) SetCreationTime(string rutaDirectorio, DateTime fechaCreación)

DateTime getLastAccessTime(string rutaDirectorio) SetCreationTime(string rutaDirectorio, DateTime FechaÚltimoAcceso) DateTime getLastWriteTime(string rutaDirectorio) SetWriteTime(string rutaDirectorio, DateTime FechaÚltimaModificación)

• Acceso al contenido de los directorios: Como sabemos, todo directorio puede

contener múltiples ficheros y/o otros directorios. Pues bien, para obtener la lista de nombres de todos los ficheros contenidos en un cierto directorio puede usarse el método estático string[] GetFiles(string rutaDirectorio) de Directory, para obtener la de sus directorios string[] GetDirectories(string rutaDirectorio), y para obtener la de sus ficheros y directorios string[] GetFileSystemEntries(string rutaDirectorio)

Los métodos anteriores devuelven listas completas con los nombres de todos los elementos del directorio correspondientes a cada uno. Por ejemplo, la lista completa de todos los ficheros contenidos en el directorio C:\Pruebas puede obtenerse así:

String[] contenidoPruebas = File.GetFiles(@”C:\Pruebas”); Sin embargo, como en ocasiones puede que sólo nos interese obtener los contenidos del directorio cuyos nombres sigan un cierto patrón, de los métodos anteriores también se ofrece una sobrecarga que admite como parámetro una cadena que indique el patrón que han de seguir los nombres a obtener. Este patrón puede usar el carácter ? para indicar que en su lugar se admite cualquier carácter y el carácter * para indicar que en su lugar se admite cualquier número (incluido 0) de cualesquiera caracteres. Así, los nombres de los ficheros almacenados en C:\Pruebas que empiecen por A y tengan extensión txt pueden obtenerse con:

String[] contenidoPruebas = File.GetFiles(@”C:\Pruebas”, “A*.txt”); • Navegación por el sistema de archivos: A veces puede interesar escribir las rutas

de los ficheros y/o directorios a los que se quiera acceder de manera relativa a un determinado directorio, para lo que es necesario ir cambiando de directorio actual. Esto puede ser útil porque permite que las aplicaciones accedan a ficheros instalados con ellas sin tener porqué conocer la ruta completa donde se instalaron: se accedería a los ficheros con rutas relativas a la ubicación de su ejecutable.

Una segunda utilidad de escribir las rutas relativamente es que ello puede simplificar mucho las rutas cuando se va a acceder a varios ficheros y/o directorios ubicados dentro de uno determinado. Por ejemplo, para acceder a varios ficheros ubicados en d:\pruebas\versión1 lo mejor puede ser establecer ese directorio como el actual y así podremos referenciarlos sin tener que escribir el nombre de su directorio padre.

Page 30: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Como se ve, para poder escribir las rutas de los ficheros de manera relativa a otros directorios puede necesitarse poder cambiar el directorio considerado como actual. Para ello, Directory proporciona el método estático SetCurrentDirectory(string rutaNuevoDirectorioActual), y también incluye un string GetCurrentDirectory() que permite consultar cuál es el en cada instante el directorio considerado como actual. Para ayudarnos en nuestros movimientos a través de los árboles de directorios se ofrece también un método DirectoryInfo GetParent(string rutaDirectorio), que devuelve el objeto DirectoryInfo que representa al padre del directorio indicado. Hay sistemas operativos que trabajan con múltiples unidades de disco lógicas, que se corresponden con los distintos dispositivos de almacenamiento de la máquina sobre la que corren o con porciones lógicas de ellos y que tendrán su propio árbol de directorios. Para facilitar la navegación en estos casos, en Directory se ha incluido un método string GetDirectoryRoot(string rutaDirectorio) que devuelve el nombre del directorio raíz del árbol asociado a la unidad de disco a la que pertenece al directorio indicado en el formato <nombreUnidad>:\; y un método string[] GetLogicalDrives() que devuelve una tabla con los nombres, en el formato anterior, de todas y cada una de las unidades de disco lógicas de la máquina sobre la que se ejecuta.

• Modificación de los directorios del sistema de archivos: Para realizar las típicas

operaciones de creación, borrado y movimiento de directorios la clase Directory incluye los métodos estáticos DirectoryInfo CreateDirectory(string rutaDirectorio), Delete(string rutaDirectorio) y Move(string rutaFuente, string rutaDestino)

Tenga en cuenta que las operaciones que realice sobre un directorio afectan a todos los ficheros y subdirectorios contenidos en el mismo. Por ello, si borra un directorio éste ha de estar vacío o si no Delete() producirá una IOException para evitar borrados accidentales, aunque de todas formas puede pasarle true como segundo parámetro para indicarle explícitamente su deseo de borrar el contenido del directorio. Tenga también en cuenta que operaciones como CreateDirectory() o Move() lanzarán IOExceptions si los directorios a crear (en el caso de Move() sería el de rutaDestino) ya existen; y que Delete() producirá una DirectoryNotFoundException si el directorio a borrar no existe. Para comprobar cómodamente la existencia de directorios y evitar así problemas puede usar el método bool Exists(string rutaDirectorio) de Directory.

Manipulación de rutas

Detección de cambios en el sistema de archivos A través de objetos FileSystemWatcher la BCL proporciona un servicio de vigilancia del estado del sistema de archivos mediante el que es posible detectar cualquier cambio que se produzca en él (creación y borrado de archivos, modificación de sus atributos, etc.) y asociar código a ejecutar en esos casos. Es tal su potencia que incluso permite detectar cambios en sistemas de archivos de máquinas remotas.

Page 31: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

De servicios como éste pueden especialmente beneficiarse aplicaciones que funcionen en una línea similar a Microsoft Biztalk Server. Es decir, que tomen datos que se les coloquen como ficheros en ciertos directorios y los procesen de alguna manera, ya sea transformándolos, extrayendo información de ellos, pasándolos a otras máquinas, etc. Ahora bien, al usar FileSystemWatcher tenga en cuenta que tiene ciertas limitaciones: sólo funciona bajo Windows N 4.0, 2000 ó XP y la detección remota no funciona entre dos máquinas que usen Windows NT 4.0. Tampoco admite la vigilancia del contenido de unidades de CD-ROM o DVD, aunque por otra parte ello es lógico en tanto que este tipo de unidades son de sólo lectura y por tanto nunca se producirán cambios en ellas.

Selección de ficheros a vigilar La forma más sencilla de indicar qué ficheros y directorios ha de vigilar un objeto FileSystemWatcher es especificando en su propiedad string Path el directorio que los contiene. Por ejemplo, el contenido de c:\datos puede vigilarse así: FileSystemWatcher observador = new FileSystemWatcher(); observador.Path = @”c:\datos” Otra posibilidad sería especificar directamente el directorio en su constructor: FileSystemWatcher observador = new FileSystemWatcher(@”c:\datos”); En ambos casos tenga en cuenta que el observador vigilará el contenido del directorio que se le ha especificado aunque posteriormente dicho directorio sea renombrado. Ello lo consigue gracias a que lo que para él lo identifica no es su nombre sino el manejador que le asocia el sistema operativo, y éste no cambia por mucho que sea renombrado. Los observadores creados como se ha dicho detectarán los cambios que se produzcan en cualquiera de los ficheros y subdirectorios del directorio que se les indique, pero si se desea que sólo se centre en algunos de ellos puede configurarse su propiedad string Filter para ello. A esta propiedad se le pasaría una cadena en la que se indicaría el patrón que seguirán los ficheros a vigilar, representando en ella cualquier carácter mediante un ? y cualquier combinación de cualesquiera caracteres con un *. Por ejemplo, para sólo vigilar los ficheros de extensión txt de c:\datos podría hacerse: FileSystemWatcher observador = new FileSystemWatcher(@”c:\datos”); observador.Path=”*.txt”; Nuevamente, otra posibilidad sería especificar el filtro directamente en el constructor: FileSystemWatcher observador = new FileSystemWatcher(@”c:\datos”, “*.txt”); En cualquier caso, hay que señalar que, por eficiencia, cuando se crea un observador este sólo vigila por defecto el contenido del directorio que se les indique pero no el de los subdirectorios del mismo. Si queremos que ello también ocurra hay que configurar explícitamente su propiedad bool IncludeSubdirectories. Por tanto, todo el sistema de archivos de la unidad C:\ de una máquina podría vigilarse así: FileSystemWatcher observador = new FileSystemWatcher(@”c:\”);

Page 32: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

observador.IncludeSubdirectories; Nótese que con IncludeSubdirectories indica que desea vigilar no sólo el contenido del directorio indicado y sus subdirectorios sino también el de los subdirectorios de éstos, y el de los subdirectorios de los subdirectorios de estos, etc. En el lado opuesto, también es posible configurar un FileSystemWatcher para que sólo vigile los cambios que se produzcan en un fichero concreto. Para ello basta asignar a su propiedad Path la ruta del directorio al que pertenece y a Filter el nombre de ese fichero. Por ejemplo, para vigilar el fichero c:\datos\dato1.dat puede hacerse: FileSystemWatcher observador = new FileSystemWatcher(@”c:\datos”, “dato1.dat”);

Selección de cambios a vigilar Una vez configurados los ficheros que se desean vigilar, los observadores por defecto sólo vigilan aquellos cambios de tamaño, nombre o fecha de última modificación que se produzcan en los mismos. Esto puede cambiarse modificando su propiedad NotifyFilters NotifyFilter, en la que se indica cuáles son los cambios ha vigilar usando un objeto de un tipo enumerado que admite los siguientes literales:

Literal Significa que se detectan cambios en... Attributes Atributos de ficheros CreationTime Fecha de creación de ficheros DirectoryName Nombres de directorios FileName Nombres de ficheros LastAccess Fecha de último acceso de ficheros LastWrite Fecha de última modificación de ficheros Size Tamaño de ficheros Security Permisos de seguridad de ficheros

Tabla 7: Significado de los literales de NotifyFilters

Estos literales puede combinarse con operaciones lógicas OR. Por ejemplo, los cambios de nombre o atributos producidos en los ficheros de c:\datos pueden detectarse con: FileSystemWatcher observador = new FileSystemWatcher(@”c:\datos”); observador.NotifiyFilter = NotifyFilters.DirectoryFile | NotifyFilters.Attributes;

Detección síncrona de cambios Una vez configurados los ficheros a vigilar y los tipos de cambios a detectar, la forma más sencilla de usar FileSystemWatcher para detectar cambios de ese tipo es usando su método WaitForChangedResult WaitForChanged( WatcherChangeTypes tipoCambio), lo que dejaría el código bloqueado en espera de que se produjese algún cambio del tipo indicado en tipoCambio. Este parámetro es de una enumeración cuyos los literales son:

Literal Tipo de cambio detectado Changed Cambio en atributos, fechas, tamaño o permisos de seguridad de fichero Renamed Cambio en nombre de fichero o directorio

Page 33: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Created Creación de fichero o directorio (no incluye renombrado de preexistentes)Deleted Borrado de fichero o directorio All Cualquiera

Tabla 8: Literales indicadores de tipo de cambio de WatcherChangeTypes

El objeto WaitForChangedResult que devuelve este método contiene información sobre el cambio producido a partir de la que podríamos actuar en consecuencia. Su propiedad string Name {get;} almacena el nombre del fichero o directorio afectado por el cambio, WatcherChangeTypes ChangeType {get;} el tipo de cambio producido, y string OldName {get;} su nombre anterior si fue un cambio de renombrado (o null si fue de otro tipo) Por ejemplo, el siguiente programa se queda en espera de que se realice algún cambio en c:\test y luego muestra un mensaje indicando el tipo de cambio producido: using System; using System.IO; class EsperaCambio { static void Main() {

if (!Directory.Exists(@"c:\test"); Directory.CreateDirectory(@"c:\test"); FileSystemWatcher observador = new FileSystemWatcher(@”c:\test”); WatcherChangedResult cambio =

observador.WaitForChanged(WatcherChangesTypes.All);

switch(cambio.ChangeType) { case WatcherChangeTypes.Changed: Console.WriteLine(“Detectado cambio en información sobre {0}”, cambio.Name)); break; case WatcherChangeTypes.Renamed: Console.WriteLine(“Detectado renombrado de {0} por {1}”,

cambio.Name, cambio.OldName)); break; case WatcherChangeTypes.Created: Console.WriteLine(“Detectada creación de {0}”, cambio.Name,)); break; case WatcherChangeTypes.Deleted: Console.WriteLine(“Detectado borrado de {0}”, cambio.Name)); break; } }

} Nótese que el problema de utilizar un mecanismo como este para detectar los cambios es que deja bloqueado al hilo que llama a WaitForChanged() hasta que se produzca el cambio, lo que puede reducir mucho el rendimiento de la aplicación si éste tarda en producirse (o incluso bloquearla si nunca llega a producirse) Para evitar esto y dar la posibilidad de que se puedan realizar otras tareas mientras llega el evento puede especificarse un como segundo parámetro de tipo int al llamar a

Page 34: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

WaitForChanged() que para indicar el tiempo máximo, en ms., que se de esperarse a que ocurra el cambio, y si no ocurre pasado ese tiempo finalizará la ejecución del método y se marcará a true la propiedad bool TimedOut del WaitForChangedResult para indicarlo.

Detección asíncrona de cambios La opción anterior de acotar el tiempo máximo de espera de WaitForChanged() para poder realizar otras tareas mientras llegue el cambios esperado permite muchas veces notables mejoras de eficiencia. Sin embargo, una solución asíncrona donde se indique al FileSystemWatcher qué ha hacerse ante cada tipo de cambio y se le deje funcionando en un hilo aparte que se encargará de detectarlos y realizar las acciones apropiadas puede ser mejor, pues en máquinas con varios procesadores es más eficiente y además da lugar a códigos más claros donde no se intercalan continuas llamadas a WaitForChanged() con sus consiguientes esperas acotadas y comprobaciones de TimedOut asociadas. Para asociar códigos a ejecutar ante cada tipo de cambio detectado FileSystemWatcher proporciona los siguientes eventos: Evento Su código asociado se ejecutará cada vez que se produzca... Changed Cambio en atributos, fechas, tamaño o permisos de seguridad de fichero Renamed Cambio en nombre de fichero o directorio Created Creación de fichero o directorio (no incluye renombrado de preexistentes) Deleted Borrado de fichero o directorio Tabla 9: Eventos de FileSystemWatcher para respuesta a cambios en el sistema de archivos

La mayoría de ellos del tipo delegado FileSystemEventHandler definido como sigue: void FileSystemEventHandler(object emisor, FileSystemEventArgs args) El emisor es el FileSystemWatcher que detectó el evento, mientras que args es donde se información sobre el fichero o directorio al que afectó el cambio. Esta información incluye su nombre en una propiedad string Name {get;}, su ruta completa en string FullPath {get;} y el tipo de cambio detectado en WatcherChangeTypes ChangeType {get} En realidad no todos los eventos de la Tabla 9 son del tipo FileSystemEventHandler, sino que en el caso concreto de Renamed su tipo delegado es la subclase del anterior: void RenameEventHandler(object emisor, RenameEventArgs args) Esto se debe a que aparte de la información proporcionada por el tipo delegado anterior, ante un renombrado puede convenir disponer también de información sobre el fichero o directorio afectado antes de ser renombrardo. Eso es precisamente lo que se aporta el segundo parámetro de este tipo de delegado, que es de un tipo RenameEventArgs hijo de FileSystemEventArgs, pues añade propiedades destinadas a almacenar el nombre (string OldValue {get;}) y ruta completa (string OldFullPath {get;}) del fichero o directorio antes de ser renombrado. Un ejemplo de cómo mostrar un mensaje cada vez que se detecte la creación de ficheros en c:\pruebas con la ruta completa del fichero creado es el siguiente trozo de código:

Page 35: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

FileSystemWatcher observador = new FileSystemWatcher(@”c:\pruebas”); observador.Created += new FileSystemEventHandler(muestraMensaje); donde el método muestraMensaje() estaría definido como sigue en alguna parte dentro de la misma definición de tipo a la que perteneciese el código anterior: void muestraMensaje(object emisor, FileSystemEventArgs args) { Console.WriteLine(“Creado fichero {0}”, args.FullPath); } Hay que tener en cuenta que una vez asociado los códigos de respuesta a los diferentes eventos del FileSystemWatcher que queramos controlar hay que poner true su propiedad bool EnableRaisingEvents para lazar el hilo que hará las detecciones de cambios y llamadas a los códigos asociados a los eventos. Además, en cualquier momento es posible parar dicho hilo sin más que volver a poner a false esta propiedad. FileSystemWatcher ha sido definido como un componente4, por lo que también podrá crear objetos suyos arrastrándolos desde la caja de herramientas de Visual Studio.NET hasta su ventana de diseño. Sin embargo, si lo hace tenga cuidado ya que entonces el valor que se le dará por defecto a su propiedad EnableRaisingEvents es true.

Ejemplo: Cifrador de directorios A modo de ejemplo de cómo usar FileSystemWatcher asíncronamente a continuación se muestra un ejemplo de aplicación que encripta todos los ficheros que coloquen en el directorio que se le pase como parámetro al lanzarla:

using System; using System.IO;

class Cifrador {

static void Main(string[] args) {

try { FileSystemWatcher observador = new FileSystemWatcher(args[0]); observador.Created+=new FileSystemEventHandler(ficheroCreado); observador.EnableRaisingEvents=true; Console.WriteLine("Cifrado de {0} activado", args[0]); Console.ReadLine(); Console.WriteLine("Cifrado de {0} desactivado", args[0]); } catch (DirectoryNotFoundException) { Console.WriteLine("{0} no es un directorio válido"); } catch (IndexOutOfRangeException) { Console.WriteLine("Error: Llamada incorrecta. Formato correcto de uso:"); Console.WriteLine("Cifrador <rutaDirectorio>");

4 Un componente es cualquier tipo que derive de Component. Sin embargo, no es el objetivo de este tema estudiar cómo se crean componentes y su explicación se posterga a temas posteriores.

Page 36: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

} } static void ficheroCreado(object objeto, FileSystemEventArgs args) { if (File.Exists(args.FullPath)) // Sólo ciframos los ficheros. Los directorios no. {

BinaryReader lectorFichero = new BinaryReader(File.OpenRead(args.FullPath)); long tamañoFichero = lectorFichero.BaseStream.Length; int[] cifradoFichero = new int[tamañoFichero]; for (int i=0; i<tamañoFichero; i++) cifradoFichero[i] = lectorFichero.ReadByte() + 3;

lectorFichero.Close(); BinaryWriter escritorFichero = new BinaryWriter(File.OpenWrite(args.FullPath)); for (int i=0; i<tamañoFichero; i++) escritorFichero.Write((byte) cifradoFichero[i]); escritorFichero.Close(); } } } Como puede observar el código del programa es muy sencillo, simplemente comprueba que se le haya pasado como argumento la ruta de algún directorio; y si es así crea un observador que vigila dicho directorio y que cada vez que se crea algún fichero sobre el mismo lo cifra incrementado en tres unidades el valor cada uno de sus bytes (el clásico criptsosistema de César)

Problemas de desbordamientos del buffer de cambios Cada FileSystemWatcher cuenta con un buffer interno donde encola temporalmente información sobre cambios producidos en el directorio que vigila mientras estaba procesando, para así procesarlos en orden en cuanto termine con el que estaba. Ahora bien, si se encolasen tantos que se saturarse ese buffer y se perdiese la detección de los que no cupiesen en él, el observador lanzaría un evento ErrorEventHandler Error al que para tratarlo podríamos asociarle un método con la signatura que sigue:

delegate void ErrorEventHandler(object emisor, ErrorEventArgs args ); Tenga especial cuidado al activar la propiedad IncludeSubdirectories, pues como se ha dicho implica que se vigile todo el contenido del sistema de archivos ubicado a partir del directorio indicado en la propiedad Path, y como eso pueden ser muchos archivos es más fácil que ocurran simultáneamente tantos cambios en ellos que se sature el buffer. Una forma de evitar errores en estos casos puede ser cambiar el tamaño de este buffer interno, que por defecto es de 8192 bytes. Para ello puede usarse la propiedad int InternalBufferSize del FileSystemWatcher, a la que se le ha de dar un valor superior a 4096 y recomendablemente (al menos en máquinas Intel) múltiplo de dicha cantidad.

Page 37: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

Otra forma para evitarlos es modificar la propiedad NotifyFilter para restringir el tipo de cambios a detectar y por tanto reducir así su número. Lo que no se le ocurra nunca es intentar reducirlos usando Filter para filtrar sólo ciertos archivos de la ruta indicada en Path, pues el buffer en realidad almacena todos los cambios producidos en dicha ruta, incluidos los que ocurran sobre los ficheros que no entren en el filtro (sólo que cuando toque procesar estos serán descartados en lugar de procesados)

Ficheros temporales Muchos sistemas operativos suelen incluir un directorio que ofrecen a las aplicaciones como almacén temporal de ficheros que sólo son útiles mientras la aplicación se esté ejecutando pero que una vez ejecutada carecen de sentido y puede eliminarse. Como según el tipo de sistema operativo que tenga instalado el usuario o según cómo lo tenga configurado la ruta de éste puede variar, la BCL incluye un método string GetTempPath() mediante el que puede obtenerse esta ruta para cada máquina concreta. Por otra parte, como este directorio es compartido entre múltiples aplicaciones, no es muy recomendable que creemos en él ficheros con nombres elegidos por nosotros mismos ya que podrían entrar en conflicto con los ya instalados por otras aplicaciones. En su lugar, es mejor usar el método string GetTempFileName () de Path, que devuelve un nombre de fichero temporal que seguro que está libre. Por ejemplo, dado el código: Console.WriteLine(Path.GetTempFileName()); El resultado de ejecutarlo en mi máquina bajo Windows NT 4.0 es que se muestra por la ventana de consola c:\TEMP\tmp9C5.tmp, si lo vuelvo a ejecutar sin modificar mi directorio temporal obtendré c:\TEMP\tmp9C6.tmp, y si lo sigo ejecutando iré obteniendo nombres similares pero con valores diferente en sus tres dígitos que en cada caso serán una unidad superior al último obtenido. En realidad el formato de estos ficheros temporales y su ubicación es lo de menos, pues son dependientes del sistema operativo y del estado del directorio temporal en cada momento. Lo que es importante que vea es que precisamente porque no siguen un patrón fijo es por lo que para trabajar con ellos debería usar los métodos arriba citados.

---------------------------------------------------------------------------------------------------------- FileShare.Inheritable no especificado y no válido al usarlo (provoca excepción de argumento fuera de rango válido) ---------------------------------------------------------------------------------------------------------- Añadir lo del paso por valor y paso por referencia ---------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------- Añadir consejos sobre cómo crear nuevos tipos de excepciones ----------------------------------------------------------------------------------------------------------

Page 38: TEMA 3: FICHEROS 2 CONCEPTOS BÁSICOS 2 … DE MATERIAS Y LIBROS/LIBROS TODOS...mismo nombre siempre y cuando se almacenen en directorios distintos. Como verá, la relación entre

---------------------------------------------------------------------------------------------------------- Añadir que la clase padre ha de ir antes que los interfaces ---------------------------------------------------------------------------------------------------------- Uso de this para resolver acceder a campos que coincidan con el nombre de variables locales. ----------------------------------------------------------------------------------------------------------Uso de get/set para tratar datos en formato diferente al de su representación interna Desde el código de una clase puede llamarase a métodos privados de otros objetos de su misma clase. Útil para acceder a campos privados en copy-constructors Otra ventaja de propiedades: al igual que los métodos, ocultan la implementación interna y facilitan la introducción de cambios en ella. ---------------------------------------------------------------------------------------------------------- El this ya no es obligatorio para llamar a los métodos de un mismo tipo ---------------------------------------------------------------------------------------------------------- Los static readonly no pueden inicializarse en constructores no static