Вы находитесь на странице: 1из 38

TEMA 3: FICHEROS................................................................................................................

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............................................................................20
ACCESO NATIVO A FICHEROS...............................................................................................................20
FICHERO BINARIOS...........................................................................................................................21
FICHEROS DE TEXTO.........................................................................................................................22
LECTURA DE FICHEROS DE TEXTO.........................................................................................................22
ESCRITURA EN FICHEROS DE TEXTO.....................................................................................................25
MANIPULACIÓN DEL SISTEMA DE ARCHIVOS........................................................................................27
MANIPULACIÓN DE FICHEROS..............................................................................................................27
MANIPULACIÓN DE DIRECTORIOS..........................................................................................................29
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
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
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
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
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:
• 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:
• 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
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.

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
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.

• Borra el directorio vacío representado por el objeto DirectoryInfo sobre el


Delete():
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
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.
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 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);

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
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:

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 él
ReadWrite 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
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

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++)
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.

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

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)

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
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:

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
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
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);
}

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.

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
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”
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

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.

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:

• 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.

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.

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:\”);


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
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
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:
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();

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.
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>");
}
}

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.

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
----------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------
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

Вам также может понравиться