Marco Besteiro y Miguel Rodríguez Introducción al lenguaje C# Introducción. C# es un lenguaje orientado a objetos (es más correcto decir orientado a componentes) diseñado para el CLR (Common Language Runtime) de la plataforma .NET que se llamó en un principio NGWS, es decir, Next Generation Windows Services. El CLR es un entorno de ejecución basado en componentes, ofrecidos por el Framework de la tecnología .NET. Visual Studio.NET soporta esta plataforma y también el lenguaje C#, así como C++, Visual Basic y los lenguajes de script VBScript y JScript. Con C# se pretende combinar la facilidad de Visual Basic y la potencia de C++. C# deriva de C y C++. Elimina características inseguras y complejas de éstos y les añade otras que ofrecen mayor comodidad. Por ejemplo: se eliminan los punteros (es posible utilizarlos en un código llamado inseguro), las macros, las plantillas, la herencia múltiple, los ficheros de cabecera, IDL, librerías de tipos... Además se añaden otras características, como orientación a objetos casi pura, seguridad de tipos, versiones, gestión automática de memoria (garbage collector)... C# se compila a un lenguaje intermedio llamado IL (MSIL) (Intermediate Language), que no es binario dependiente de la plataforma, sino que se ejecuta sobre una máquina virtual que provee el Common Language Runtime. Esto no quiere decir que C# no pueda compilarse a código binario dependiente de la plataforma. Simplemente ha sido pensado para el CLR. Existe una solución intermedia, que es la posibilidad de utilizar código nativo desde C#. El inconveniente de esta solución es que rompe la portabilidad. En definitiva, desde C# se puede utilizar no sólo el API provisto por el Framework .NET, sino que también se puede utilizar el API COM, la Automatización y las APIs de estilo C. Diferencias entre C# y C/C++. C# ha sido diseñado para ser fácilmente aceptado por los desarrolladores de C y C++. No obstante presenta importantes diferencias: - - La gestión de memoria es automática, lo cual quiere decir que el desarrollador sólo ha de preocuparse de crear un objeto cuando lo necesite pero no de eliminarlo, ya que esta tarea queda a cargo del garbage collector (recolector de basura). Este hecho cambia el sentido del destructor de una clase. No se utilizan punteros. En su lugar se utilizan referencias a objetos. No obstante, se permite utilizar punteros en casos especiales, como por ejemplo cuando es necesario acceder a código nativo (dll) al que hay que pasarle o que devuelve punteros (al código C# de este tipo se le denomina código inseguro). Existe una posibilidad de utilizar punteros en código seguro y es a través de objetos de un tipo especial llamados Delegates Su comportamiento es similar a 1/13 1 Marco Besteiro y Miguel Rodríguez - - - - - Introducción al lenguaje C# los punteros a funciones, con la ventaja de ser más seguro y respetar los tipos de datos. Se cambian ciertos aspectos referentes a los operadores: o No se utilizan los operadores -> ni ::. El único operador de acceso a métodos de objetos que se utiliza es el operador punto ‘.’ . o Se incluyen dos operadores nuevos: is y typeof. o Se cambia el funcionamiento de los operadores lógicos &, ^ y |. o La sobrecarga de operadores es diferente. Hay ciertas palabras clave que se utilizan de modo distinto, como extern y static. El método Main se declara de modo distinto. No se permiten las declaraciones adelantadas (forward). El manejo de errores se hace mediante excepciones. No se soportan las macros, aunque sí las directivas de preprocesador. No se utilizan ficheros de cabecera (.h) ni similares (IDL y librerías de tipos). El código es compilado en unidades llamadas assemblies, en las cuales se almacena la representación del código en un lenguaje intermedio llamado IL y los metadatos, que sustituyen a los ficheros .h de C++ (o a las librerías de tipos y entradas de registro). Los assemblies se pueden generar a partir de código escrito en otros lenguajes, como C++ o Visual Basic, lo cual quiere decir que desde C# se puede utilizar código IL generado a partir de otros lenguajes. Los assemblies se agrupan en namespaces (que siguen una estructura jerárquica). No existe una librería de tiempo de ejecución (Runtime) C# como en C y C++. En su lugar existe un Runtime .NET accesible a través de la clase System. El mecanismo de herencia sufre cambios: o No se permite herencia múltiple en clases pero sí en interfaces. o La sobrescritura de métodos se ha de hacer utilizando el operador explícito override. o El modificador new permite ocultar miembros heredados. Existen tres posibles grupos de tipos: o Tipos valor: son tipos primitivos como char, int, struct... C# incluye dos tipos nuevos, boolean (True o False, se utiliza en las sentencias condicionales, en lugar de integer, como se hacía en C++) y decimal (para operaciones financieras). Las variables de tipo valor se guardan en la pila. o Tipos referencia: incluye los tipos class, interface, delegate y array. Los objetos referenciados por variables de tipo referencia se guardan en el heap o montón. Existen dos tipos de referencia predefinidos, object y string. object es el tipo base de la jerarquía de clases .NET. Todo dato deriva del tipo object mediante herencia simple (sólo se permite herencia múltiple de interfaces). Todo dato de cualquier otro tipo puede convertirse a object (a esta conversión se la llama boxing) permitiéndose también la operación inversa (unboxing). Esto permite la existencia de lo que se denomina sistema de tipos unificado. o Tipos puntero: sólo se permite su utilización en código inseguro. Las sentencias sufren ciertos cambios y aparecen otras nuevas: o La sentencia switch admite string. o La sentencia foreach se puede aplicar a tipos y colecciones. 2/13 2 Marco Besteiro y Miguel Rodríguez - Introducción al lenguaje C# o Las sentencias checked y unchecked permiten controlar desbordamientos. El mantenimiento de versiones es más sencillo que en C y C++. Entre otros aspectos, la compatibilidad a nivel binario deja de ser un problema. Se utilizan Atributos (Attributes) que son anotaciones que se adjuntan a una clase para almacenar cierta información (ficheros de ayuda, versión, etc...) y que pueden ser obtenidos mediante reflexión. Razones para utilizar C#. C# es un lenguaje elegante, sencillo, orientado a componentes y ofrece un manejo de tipos seguro. Además, C# ofrece la posibilidad de construir componentes de sistema perdurables en base a: - Soporte total a la plataforma COM para permitir la integración con código ya existente. - Posibilidad de construir código robusto gracias a la seguridad de tipos y a la gestión automática de memoria (garbage collector). - Seguridad a nivel de código basada en mecanismos de confianza. - Soporte total a los metadatos y a sus posibles extensiones. Por último, C# permite la interoperación con otros lenguajes en base a las siguientes características: - Acceso a librerías a través de COM+ y los servicios .NET. - Soporte XML para interacción basada en componentes. - Simplificación de la administración y desarrollo de aplicaciones y componentes gracias a un cuidado mecanismo de versiones. 3/13 3 Marco Besteiro y Miguel Rodríguez Introducción al lenguaje C# La plataforma .NET Podría pensarse en la plataforma .NET como un sustituto de la arquitectura Windows DNA (Distributed Network Architecture) pero sería una comparación insuficiente. La arquitectura DNA se centra en la construcción de aplicaciones de tres capas para Windows basándose en tecnologías como ASP, COM, etc... La plataforma .NET es una nueva plataforma para el desarrollo de aplicaciones en general (no necesariamente para Windows, ni de tres capas) que cubre todos los aspectos que afectan a tales aplicaciones, desde un entorno de ejecución común (CLR) hasta lenguajes específicamente diseñados para rentabilizar al máximo las características de esta plataforma. El objetivo principal de la plataforma .NET es ofrecer una red de servicios que se ejecuten sobre el CLR y que hayan sido diseñados con cualquier lenguaje compatible con el CLR (lenguajes de este tipo son C#, C++ y Visual Basic, pero podría añadirse cualquier lenguaje para el que se haya diseñado un compilador a IL, que es el lenguaje intermedio que entiende el CLR). Figura 1.1. Framework .NET El CLR ofrece las bases para poder ejecutar los servicios. Las clases base ofrecen el control de tipos de datos básico, las clases de tipo colección y de otras clases generales. Los servicios de datos permiten el acceso a los datos desde los servicios Web y Windows, para lo cual tienen clases de soporte de datos y XML. La plataforma .NET ha hecho que las siguientes tecnologías se vuelvan obsoletas: - La Librería de tiempo de ejecución C/C++ (Runtime Library) 4/13 4 Marco Besteiro y Miguel Rodríguez - Introducción al lenguaje C# STL (Standard Template Library) MFC (Microsoft Foundation Class Library) ATL (Active Template Library) WTL (Windows Template Library) ADO (ActiveX Data Objects) ASP (Active Server Pages) VBScript & VBA (Visual Basic Script y VB for Applications) COM (Component Object Model) Win32 API No obstante, es posible acceder al API Win32 o a componentes COM desde aplicaciones .NET, aunque las tecnologías emergentes son las que se recomiendan: - Runtime .NET - COM+ - ATL+ - ADO+ - ASP+ - El API de Win32 es encapsulado en la plataforma .NET por la BCL (Base Class Library). El CLR y la Librería de Clases Base (BCL, Base Class Library). El CLR es un entorno de ejecución común, capaz de ejecutar aplicaciones codificadas en un lenguaje intermedio (no dependiente de la máquina) llamado IL (MSIL). La ventaja que implica soportar un lenguaje intermedio es que las aplicaciones que se ejecutan sobre el CLR son portables. Al no depender de una máquina concreta, sólo necesitan para ejecutarse sobre cualquier máquina que el CLR esté instalado en tal máquina. Como todo entorno de ejecución, el CLR se ayuda de una librería de clases para ofrecer una serie de servicios a los desarrolladores, permitiendo que puedan desarrollar aplicaciones sencilla y rápidamente reutilizando componentes así como que puedan desarrollar componentes reutilizables. Las aplicaciones y componentes que los desarrolladores compilan con compiladores para el CLR son llamados de “código dirigido” (“managed code”). El “managed code” se beneficia de características tales como: - Soporte multi-lenguaje: cualquier lenguaje para el que se disponga de compilador a IL permitirá desarrollar aplicaciones y componentes que puedan interaccionar directamente con componentes y aplicaciones desarrollados en otros lenguajes para los que también exista compilador a IL. EL lenguaje IL sigue una especificación estándar, llamada CLS (Common Languaje Specification) que indica el formato con el que han de generar el código los compiladores a IL. Además, para que el soporte multi-lenguaje no se vea afectado por los diferentes tipos de datos de los diferentes lenguajes se ha creado un sistema de tipos común (CTS), de modo que sólo se deberán utilizar tipos del CTS si se desea la interoperabilidad con otros lenguajes. 5/13 5 Marco Besteiro y Miguel Rodríguez - - Introducción al lenguaje C# Gestión de excepciones multilenguaje: es una característica que deriva del soporte multi-lenguaje y permite el control de excepciones en aplicaciones creadas a partir de fuentes en diferentes lenguajes. Seguridad mejorada. Soporte para versiones. Modelo simple de interacción entre componentes. Servicios de debug. Metadatos. Para permitir al CLR ofrecer servicios al “managed code” los compiladores no sólo han de generar el código IL correspondiente al código fuente, sino también metadatos, los cuales se almacenarán junto con el código intermedio IL y harán innecesario el registro del sistema (los metadatos son a la plataforma .NET lo que las bibliotecas de tipos, las entradas del registro del sistema y algún que otro dato son a COM). Para un objeto de la plataforma .NET, los metadatos guardan información necesaria para utilizar el objeto, como puede ser: - El nombre del objeto. Los nombres y tipos de todos sus campos. Los nombres de todos los métodos, así como los nombres y tipos de sus parámetros. Figura 1.2. Jerarquía de los Metadatos. 6/13 6 Marco Besteiro y Miguel Rodríguez Introducción al lenguaje C# El CLR utilizará los metadatos para localizar y cargar clases, resolver invocaciones a métodos, generar código nativo, gestionar la seguridad, manejar las instancias de clases y fijar los límites del contexto de tiempo de ejecución. El CLR también se encarga de la gestión de memoria dinámica con la consecuente desaparición de errores asociados a la gestión manual de memoria. A los objetos cuyo tiempo de vida es gestionado por el CLR se les llama “managed data” (“datos dirigidos o gestionados”). Para que el CLR pueda trabajar con “managed data” es necesario que el código sea también “managed code”. No obstante hay ocasiones en que por utilizar características distintas de distintos lenguajes en el desarrollo no es posible utilizar managed code ni managed data, en este caso se habla de “unmanaged code” y “unmanaged data”. Assemblies. El CLR trabaja realmente con assemblies. Un assembly es un fichero con un formato muy parecido al formato PE (el que se utiliza en los ficheros .exe o .dll tradicionales) que contiene el código IL resultado de compilar el código fuente, los metadatos y otros ficheros necesarios para poder ejecutar el paquete o assembly. Además, un assembly contiene un fichero llamado “fichero de manifiesto” o manifest, el cual indica los assemblies de los que depende, los ficheros que contiene, controla los tipos y recursos expuestos por el assembly y establece un mapeo entre esos tipos y recursos y los ficheros que contienen esos tipos y recursos. Existen dos tipos de assemblies: - aplicaciones: tienen un punto de entrada principal y único (Main) y suelen tener extensión .exe. - librerías: no tienen un punto de entrada principal (Main) y suelen tener extensión .dll. El CLR provee APIs que los motores de scripts pueden utilizar para crear dinámicamente un assembly a partir de un script. A este tipo de assemblies se les llama assemblies dinámicos y podría ser considerado como un tercer tipo. Un assembly puede estar físicamente contenido en varios ficheros o en uno sólo. La ventaja de utilizar varios ficheros es que en caso de ser pedidos a través de la red, sólo se han de pasar los necesarios. 7/13 7 Marco Besteiro y Miguel Rodríguez Introducción al lenguaje C# Figura 1.3. Tipos de assemblies. De uno o de varios ficheros. Como puede verse, el fichero de manifiesto (manifest) puede ser almacenado de varios modos: - Para un assembly de un fichero, el manifiesto es incorporado al fichero PE. - Para un assembly de varios ficheros, el manifiesto puede almacenarse como un fichero separado o puede incorporarse en uno de los ficheros PE del assembly. El assembly que contiene los tipos básicos del CLR se llama mscorlib.dll y es el único que se toma por defecto al compilar. Si se desea utilizar un tipo que esté en otro assembly, ha de indicarse al compilador el nombre de tal assembly. Namespaces. Así como un assembly es una agrupación física de clases, otros tipos e información sobre tales, un namespace es una agrupación lógica. De este modo, al escribir el código de una aplicación y utilizar clases y otros tipos se ha de indicar (en el código) el namespace al que pertenecen tales tipos, no el assembly. Como se ha comentado, el assembly se ha de pasar como parámetro al compilador. La lógica que lleva a esta separación es muy sencilla. Supóngase que se crean 5 assemblies con tipos relacionados con el manejo de redes. Aunque físicamente son 5 ficheros distintos, lógicamente se refieren al mismo tema, de modo que podrían estar agrupados bajo un mismo nombre lógico (nótese que namespace significa “espacio de nombre”). De modo similar, un assembly puede contener clases y tipos dispares que puedan ser divididos en varios namespace. De hecho, tal es el caso del assembly mscorlib.dll en el que uno de sus namespace es System. El namespace System incluye los tipos de bajo nivel básicos, como el tipo base Object, Byte, Int32, Math y Delegate. 8/13 8 Marco Besteiro y Miguel Rodríguez Introducción al lenguaje C# Los namespace forman una jerarquía lógica, lo cual quiere decir que se pueden anidar. Por ejemplo, dentro del namespace System se encuentra el namespace Collections que incluye tipos como ArrayList, BitArray, Queue y Stack. Figura 1.4. Namespaces del Framework .NET. Se podría pensar, recordando que un assembly puede ser partido en varios ficheros, que el assembly puede también realizar la función del ejemplo comentado. Visto así es cierto, pero el hecho de partir un assembly en varios ficheros se realiza por motivos de eficiencia en la transmisión de sólo parte del assembly, no por motivos de organización lógica. Attributes. Para poder convertir una clase en un componente es necesaria información adicional sobre la clase como: - Información sobre persistencia. Modo en que han de ser manejadas las transacciones. El CLR de la plataforma .NET soporta atributos personalizados como un modo de asociar información sobre el componente a los metadatos y después poder recuperarla mediante el mecanismo de reflexión. Los atributos no son valores concretos y son extensibles, lo cual puede hacerlo el propio programador. 9/13 9 Marco Besteiro y Miguel Rodríguez Introducción al lenguaje C# JIT (Just In Time) Compiler. El CLR posee un compilador en línea o JIT que tiene como labor convertir el código IL de un assembly a código nativo de la plataforma para que sea ejecutado. Esta conversión se realiza una sola vez, si se vuelve a invocar durante la ejecución un código ya compilado a código nativo, se utiliza el código nativo. Por defecto no se elige la opción de compilar todo el assembly a nativo, ya que implica un coste de tiempo que en la mayoría de los casos no se ve justificado, ya que es raro que se ejecute todo el código del assembly en una ejecución concreta. Según lo comentado, la ejecución del código intermedio de un assembly por parte del CLR es algo así: Figura 1.5. Funcionamiento del entorno de tiempo de ejecución CLR. CTS (Common Type System). El CTS es la especificación formal del sistema de tipos implementado por el CLR. El CTS especifica cómo están definidas las clases de objetos, también llamadas “tipos”. Por ejemplo: el CTS permite a un tipo clase contener cero o más miembros, los cuales pueden ser campos, métodos, propiedades o eventos. El CTS también especifica reglas para: 10/13 10 Marco Besteiro y Miguel Rodríguez - - - Introducción al lenguaje C# La visibilidad de tipos y el acceso a miembros de un tipo. Los tipos pueden ser visibles desde el exterior de su assembly, desde los clientes de su assembly o sólo desde el código de su mismo assembly. La herencia y las funciones virtuales. La raíz de la jerarquía de herencia: todos los tipos de clase de la plataforma .NET deben derivar de un tipo de clase predefinido llamado System.Object (System es un namespace). De este modo, todos los tipos de clase que se definan heredarán las características de la clase Object. El ciclo de vida de un objeto. Si se desarrolla una aplicación para la plataforma .NET, independientemente del lenguaje utilizado, ésta debe ajustarse al CTS. Esto implica que no siempre van a poder ser utilizadas todas las características de un lenguaje determinado. Por ejemplo, si se desarrolla con C++ para la plataforma .NET, aunque este lenguaje soporte la herencia múltiple no puede ser utilizada (en este caso concreto, el compilador para la plataforma .NET de Visual C++ avisa del error). Figura 1.6. Sistema de Tipos del Framework .NET. CLS (Common Language Specification) El CLR de la tecnología .NET permite generar un assembly partiendo de varios lenguajes, de modo que los objetos de los diferentes lenguajes puedan interoperar entre sí. Esto es posible gracias a que el CLR tiene un conjunto de tipos estándar, metadatos para hacer que la información de tipos sea auto descriptiva y un entorno de ejecución común. Al intentar llevar a cabo esta interoperabilidad entre lenguajes el primer problema que aparece son las diferencias que hay entre los diversos lenguajes. Para salvar este 11/13 11 Marco Besteiro y Miguel Rodríguez Introducción al lenguaje C# problema a la hora de crear tipos .NET que sean fácilmente accesibles desde otros lenguajes de programación es necesario utilizar características de cada lenguaje que sean compatibles con los demás. Para hacer esta labor más sencilla, Microsoft ha definido el CLS, que informa a los proveedores de compiladores del conjunto mínimo de características que han de soportar sus compiladores para adaptarse al CLR. Se dice que un tipo es compatible con el CLS si todas sus partes accesibles públicamente: - Están compuestas sólo por tipos compatibles con el CLS. - En caso de que alguna no esté compuesta por tipos compatibles con el CLS ha de estar marcada explícitamente como no compatible con el CLS. Se dice que una herramienta es un consumidor compatible con el CLS si puede utilizar íntegramente cualquier tipo compatible con el CLS, es decir, llamar a cualquier método compatible con el CLS, crear una instancia de cualquier tipo compatible con el CLS, leer y modificar cualquier campo compatible con el CLS, etc... Por último, se dice que una herramienta es un extensor compatible con el CLS si es un consumidor compatible con el CLS y además puede extender cualquier clase base compatible con el CLS, implementar cualquier interface compatible con el CLS, definir nuevos interfaces compatibles con el CLS y situar cualquier atributo compatible con el CLS en todos los elementos de metadatos apropiados. Sistema de Ejecución Virtual (Virtual Execution System, VES) El Sistema de Ejecución Virtual implementa y refuerza el modelo CTS. Es responsable de cargar y ejecutar los programas escritos para el CLR. El VES ofrece los servicios necesarios para ejecutar “managed code” y “managed data”, así como utilizar los metadatos para conectar en tiempo de ejecución (“enlace tardío” o “late binding”) módulos que han sido generados por separado. Verificación y Seguridad. La seguridad es inherente al diseño de la plataforma .NET. Entra en acción tan pronto como una clase es cargada realizando chequeos para verificar el código (seguridad de tipos...). Tras los chequeos controla el acceso del código a los recursos. La seguridad de la plataforma .NET ofrece mecanismos para controlar la identidad y las posibilidades del código. Estos mecanismos alcanzan los límites del contexto, del proceso y de la máquina para asegurar la seguridad de los datos en escenarios remotos. Estos mecanismos de seguridad trabajan y extienden los mecanismos de seguridad del Sistema Operativo. Las áreas que abarca el mecanismo de seguridad de la plataforma .NET son: - Seguridad de tipos: el código que respeta la seguridad de tipos sólo referencia memoria que haya sido reservada para su uso y sólo accede a los objetos a través de sus interfaces. El CLR cumple la seguridad de tipos combinando un sistema de tipos fuerte en los metadatos (parámetros, miembros y elementos de arrays, 12/13 12 Marco Besteiro y Miguel Rodríguez - - - - - - - - - Introducción al lenguaje C# valores de retorno de métodos y valores estáticos) con un sistema de tipos fuerte en el lenguaje intermedio MSIL (variables locales y manejo de la pila). Es posible saltar la verificación utilizando justificantes de confianza. Identificación de código: sólo existen dos modos para que el código se pueda ejecutar, a través del “class loader” (cargador de clases) o a través de los servicios de interoperabilidad. Ambos son proporcionados por el CLR y se basan en información que se conoce (como puede ser la dirección de origen del código, o la identidad de quien lo ha publicado...), la cual cotejan con la que extraen al cargar el código. Seguridad de acceso del código: Esta seguridad se basa en pólizas o justificantes asociados a los assemblies en las que se indican conjuntos de permisos que especifican qué es lo que puede y no puede hacer el código de un assembly. Permisos sobre recursos: Estos permisos son utilizados para saber si el código tiene derechos de acceso a recursos concretos en tiempo de carga y/o en tiempo de ejecución. Permisos de identificación: se basan en datos de un assembly como el lugar de origen, quién lo ha publicado, el nombre compartido del assembly... y permiten controlar el acceso a los métodos de las clases (son similares en cuanto a su origen a la identificación de código). Seguridad declarativa: es un mecanismo muy potente que permite insertar controles de seguridad en las clases, campos o métodos mediante anotaciones. Seguridad imperativa: es similar a la declarativa, pero en lugar de anotaciones se utiliza código, con lo cual se pueden realizar controles de seguridad dinámicos, es decir, durante la ejecución. Seguridad basada en pólizas o justificantes: Este tipo de seguridad consiste en crear archivos o pólizas que indiquen qué puede hacer o no un determinado código en función de su origen... (por ejemplo: se puede confiar en código local y en código del servidor ‘x’ pero no en código del servidor ‘y’). Seguridad basada en roles: es similar a la basada en pólizas pero en este caso es el papel de un código en una cierta situación el que determina lo que puede hacer. Por ejemplo, se puede limitar el dinero a transferir en una transacción dependiendo de quien la haga y en que entidad bancaria. Seguridad remota: Cuando se superan los límites de una máquina, ciertos aspectos de la seguridad como la autenticación, autorización, confidencialidad e integridad se vuelven críticos. La plataforma .NET ha de proveer soporte para estos mecanismos de un modo compatible con los protocolos de red existentes y con la infraestructura remota. De este modo, las aplicaciones podrán soportar este tipo de seguridad de un modo cómodo. Criptografía: La plataforma .NET ofrece un conjunto de objetos criptográficos que soportan los algoritmos conocidos y su utilización más común (hashing, encriptación y firmas digitales). Los objetos criptográficos son utilizados por la plataforma .NET para soportar servicios internos y además están disponibles para los desarrolladores que necesiten soporte criptográfico. 13/13 13