Buscar en documentos con Microsoft Index Server

Si deseamos realizar una búsqueda de texto en documentos, .NET nos permite aprovechar la potencia del Servicio de Microsoft Index Server para búsqueda e indexación de documentos. Si además necesitamos filtrar por un tipo de documento concreto lo podemos usar en combinación con Ifilter.

Primero hemos de asegurarnos de que el Servicio de Microsoft Index Server está iniciado. De no ser así, accederemos al listado de servicios de nuestra máquina y lo pondremos en marcha. Si deseamos que permanezca más allá de una sesión, habilitaremos el inicio automático del servicio.

Microsoft .NET permite el acceso a Index Server mediante OleDb, de forma que podemos crear consultas SQL sobre los índices de los documentos que nos permitan crear datasets sobre esos resultados como cualquier otro origen de datos. En el IDE, desde el explorador de servidores se puede crear una conexión con dicho servicio.

Un ejemplo de consulta SQL sobre Index Server podría ser el siguiente:

SELECT Rank, VPath, DocTitle, Filename, Characterization, Write

FROM SCOPE()

WHERE FREETEXT(‘diseño’)

ORDER BY RANK, SIZE DESC

Rank es el campo que establece el rango de coincidencia del patrón dado por FREETEXT(). Si añadimos más palabras a esta función, la consulta nos devolverá aquellos documentos que tengan al menos una de las palabras definidas en el patrón.

Otra función de búsqueda interesante es CONTAINS

SELECT Rank, VPath, DocTitle, Filename, Characterization, Write

FROM SCOPE()

WHERE NOT CONTAINS(VPath, ‘”_vti_” OR “.config”‘)

AND CONTAINS(Contents, ‘”keyword1″ AND “keyword2″‘)

AND CONTAINS(DocTitle, ‘”keyword1″ AND “keyword2″‘)

La función SCOPE nos permite limitar nuestra consulta a uno varios directorios particulares, y definir si queremos o no que incluya subdirectorios.

Usando funciones de rango y particionado en SQL Server

Una de las nuevas funcionalidades que nos ofrece SQL Server 2005 es la de poder añadir expresiones de ranking a nuestra consulta. Esto viene muy bien en aplicaciones .NET para paginar y ordenar en un grid además de en cualquier otro escenario. Vamos a ver estas nuevas funcionalidades en la base de datos de ejemplo AdventureWorks.

ROW_NUMBER()

La función más básica de ranking es ROW_NUMBER(). Esta función devuelve una columna como una expresión que contiene el número de fila dentro del conjunto de resultados. Este número solo se usa en el contexto de la consulta, si el resultado de ella cambia, ROW_NUMBER cambiará. La expresión ROW_NUMBER toma una sentencia ORDER BY con la columna que queremos usar para contar las filas con un operador OVER como se muestra a continuación:

SELECT SalesOrderID, CustomerID, Row_Number() Over (Order By SalesOrderID) as RunningCount

FROM Sales.SalesOrderHeader

WHERE SalesOrderID > 10000

ORDER BY SalesOrderID

DevolverÍa el siguiente resultado:

SalesOrder     ID Customer   ID RunningCount
——–     ———-   ————–
43659            676                  1
43660            117                   2
43661            442                  3
43662            227                  4
43663           510                    5
43664           397                   6
43665           146                    7
43666          511                      8
43667          646                     9
More

Alternativamente, si tenemos una clÁusula ORDER BY en nuestro conjunto de resultados diferente de la ORDER BY en nuestra expresión ROW_NUMBER()

Row_Number usando un valor único, diferente order by

SELECT SalesOrderID, CustomerID, Row_Number() Over (Order By SalesOrderID) as RunningCount

FROM Sales.SalesOrderHeader

WHERE SalesOrderID > 10000

Order By

CustomerID Diferente ORDER BY que en Row_NUMBER

Muestra lo siguiente:

SalesOrder     ID Customer   ID RunningCount
——–     ———-   ————–
43860             1                       202
44501             1                       843
45283             1                      1625
46042             1                      2384
46976             2                     3318
47997             2                     4339
49054             2                     5396
More

Si elegimos la function ROW_NUMBER() contra una columna no única, se producirá un RunningCount de forma que no haya filas que tengan el mismo número. Por ejemplo, CUSTOMERID se puede repetir en este ejemplo:

SELECT SalesOrderID, CustomerID, Row_Number() Over (Order By CustomerID) as RunningCount

FROM Sales.SalesOrderHeader

WHERE SalesOrderID > 10000

Order By

CustomerID

Esta consulta muestra lo siguiente:

SalesOrder     ID Customer   ID RunningCount
——–     ———-   ————–
43860            1                       1
44501             1                      2
45283            1                       3
46042           1                       4
46976           2                      5
47997           2                      6
49054           2                      7
50216            2                      8
51728            2                      9
57044           2                      10
63198           2                       11
69488          2                      12
44124          3                       13
. . . More

También podemos usar RANK como expresión en una cláusula WHERE. SQL Server devuelve un error cuando intentamos referirnos a una función de Ranking en una cláusula WHERE. Podemos solucionar esto usando una Common Table Expression.

  • usar una common table expression si se quiere
  • filtrar por una de las filas que contenga
  • una función de ranking, ya que estas
  • no están permitidas en cláusulas where o having

WITH NumberRows

AS

(

SELECT SalesOrderID, CustomerID, Row_Number() Over (Order By SalesOrderID) as RowNumber

FROM Sales.SalesOrderHeader

)

SELECT * FROM NumberRows

WHERE RowNumber BETWEEN 100 AND 200

ORDER BY SalesOrderID

El resultado se muestra a continuación:

SalesOrder     ID Customer   ID RunningCount
——–     ———-   ————–
43758            27646              100
43759            13257               101
43760           16352               102
43761            16493               103
43762            27578               104
43763            16525                105
43764            16612                106
43765            11010                107
43766            16518               108
43767             11001                109
43768 27649 110
43769 21659 111
43770 27614 112
�More

De la misma forma, podemos usar una CTE (Common Table Expression) si necesitamos establecer el rango por un agregado GROUP BY. En este caso, usaremos SUM() en una CTE y aplicaremos entonces ROW_NUMBER() despu�s del agregado.

�rango por la suma de totaldue

�necesitamos una CTE para hacer una suma

�de forma que este ejemplo tendr� un

�customerID sumado con todos de sus propios pedidos

With CustomerSum

As

(

Select

CustomerID, Sum(TotalDue) As totalamt

From

Sales.SalesOrderHeader

Group By

CustomerID

)

�a�ade un row_number al final de los resultados

Select

*, Row_Number() Over (Order By totalamt Desc) as RowNumber

From

CustomerSum

Resultado:

CustomerID totalamt RowNumber
���� ������� �������
678 1179857.4657 1
697 1179475.8399 2
170 1134747.4413 3
328 1084439.0265 4
514 1074154.3035 5
155 1045197.0498 6
72 1005539.7181 7
227 984324.0473 8
433 983871.933 9
166 979881.3491 10
146 964134.7777 11
670 946105.7121 12
506 937466.3027 13
�More

RANK()

RANK() trabaja de forma similar a ROW_NUMBER() excepto porque no deshace empates, es decir, no vamos a obtener un �nico valor en caso de empate.

Select

SalesOrderID, CustomerID, RANK() Over (Order By CustomerID) as RunningCount

From

Sales.SalesOrderHeader

Where

SalesOrderID > 10000

Order By

CustomerID

El resultado devuelto:

SalesOrderID CustomerID RunningCount
���� ���� �������
43860 1 1
44501 1 1
45283 1 1
46042 1 1
46976 2 5
47997 2 5
49054 2 5
50216 2 5
51728 2 5
57044 2 5
63198 2 5
69488 2 5
44124 3 13
. . . More

El siguiente ejemplo redondea el campo TotalDue a la centena m�s cercana usando CTE y RANK sobre el campo derivado:

�rango por TotalDue, sumado y redondeado a la centena

�necesitamos una CTE para hacer la suma y redondear

�asi que este ejemplo obtendr� el total de todos

�los pedidos por CustomerID

With CustomerSum

As

(

Select

CustomerID, Round(Convert(Int, Sum(TotalDue))/100,8) *100 As totalamt

From

Sales.SalesOrderHeader

Group By

CustomerID

)

Select

*, Rank() Over (Order By totalamt Desc) as Rank

From

CustomerSum

La consulta mostrar� lo siguiente:

CustomerID totalamt Rank
���� ���� �������
678 1179800 1
697 1179400 2
170 1134700 3
328 1084400 4
514 1074100 5
155 1045100 6
72 1005500 7
227 984300 8
433 983800 9
166 979800 10
146 964100 11
670 946100 12
�More

DENSE_RANK() y NTILE(n)

DENSE_RANK() trabaja ex�ctamente como lo hace RANK() pero eliminando el salto entre n�meros en caso de empate.

Select

SalesOrderID, CustomerID, DENSE_RANK() Over (Order By CustomerID) as RunningCount

From

Sales.SalesOrderHeader

Where

SalesOrderID>10000

Order By

CustomerID

As� queda el resultado de la consulta:

SalesOrderID CustomerID RunningCount
���� ���� �������
43860 1 1
44501 1 1
45283 1 1
46042 1 1
46976 2 2
47997 2 2
49054 2 2
50216 2 2
51728 2 2
57044 2 2
63198 2 2
69488 2 2
44124 3 3
. . . More

NTile(n) divide todos los resultados en partes aproximadamente iguales y asigna cada parte al mismo n�mero dentro del resultado de la consulta. Esto puede ser muy �til en porcentajes. Veamos un ejemplo:

Select

SalesOrderID, CustomerID, NTILE(10000) Over (Order By CustomerID) as RunningCount

From

Sales.SalesOrderHeader

Where

SalesOrderID > 10000

Order By

CustomerID

Nos dar�a algo as�:

SalesOrderID CustomerID RunningCount
���� ���� �������
43860 1 1
44501 1 1
45283 1 1
46042 1 1
46976 2 2
47997 2 2
49054 2 2
50216 2 2
51728 2 3
57044 2 3
63198 2 3
69488 2 3
44124 3 4
. . . More

Un �ltimo ejemplo nos muestra c�mo usar todas estas funciones juntas en una sola consulta y de paso nos ense�a la diferencia entre las cuatro funciones de rango.

Select

SalesOrderID as OrderID,

CustomerID,

Row_Number() Over (Order By CustomerID) as RowNum,

RANK() Over (Order By CustomerID) as Rank,

DENSE_RANK() Over (Order By CustomerID) as DRank,

NTILE(10000) Over (Order By CustomerID) as NTile

From

Sales.SalesOrderHeader

Where

SalesOrderID > 10000

Order By

CustomerID

OrderID CustomerID RowNum Rank DRank NTile
���� ���� ��- �� �� ���
43860 1 1 1 1 1
44501 1 2 1 1 1
45283 1 3 1 1 1
46042 1 4 1 1 1
46976 2 5 5 2 2
47997 2 6 5 2 2
49054 2 7 5 2 2
50216 2 8 5 2 2
51728 2 9 5 2 3
57044 2 10 5 2 3
63198 2 11 5 2 3
69488 2 12 5 2 3
44124 3 13 13 3 4
44791 3 13 13 3 4
�More

PARTITION BY

Las funciones de rango tambi�n se pueden combinar con funciones de particionado. Este tipo de funciones divide un conjunto de resultados en particiones iguales basadas en los valores de una sentencia PARTITION BY en conjunci�n con la cl�usula OVER de la funci�n de rango. Es como aplicar un GROUP BY a nuestra funci�n de rango obteniendo un rango separado para cada partici�n. El siguiente ejemplo usa ROW_NUMBER con PARTITION BY para contar el n�mero de pedidos por fecha de pedido y por vendedor. Hacemos esto con un PARTITION BY SalesPersonID OVER OrderDate. Puede ser usado con cualquiera de las cuatro funciones de rango.

Select

SalesOrderID, SalesPersonID, OrderDate, Row_NUMBER() Over (Partition By SalesPersonID Order By OrderDate) as OrderRank

From

Sales.SalesOrderHeader

Where

SalesPersonID is not null

Resultado:

SalesOrderID SalesPersonID OrderDate OrderRank
���� ����- �������� ���
43659 279 2001-07-01 00:00:00.000 1
43660 279 2001-07-01 00:00:00.000 2
43681 279 2001-07-01 00:00:00.000 3
43684 279 2001-07-01 00:00:00.000 4
43685 279 2001-07-01 00:00:00.000 5
43694 279 2001-07-01 00:00:00.000 6
43695 279 2001-07-01 00:00:00.000 7
43696 279 2001-07-01 00:00:00.000 8
43845 279 2001-08-01 00:00:00.000 9
43861 279 2001-08-01 00:00:00.000 10
. . . More
48079 287 2002-11-01 00:00:00.000 1
48064 287 2002-11-01 00:00:00.000 2
48057 287 2002-11-01 00:00:00.000 3
47998 287 2002-11-01 00:00:00.000 4
48001 287 2002-11-01 00:00:00.000 5
48014 287 2002-11-01 00:00:00.000 6
47982 287 2002-11-01 00:00:00.000 7
47992 287 2002-11-01 00:00:00.000 8
48390 287 2002-12-01 00:00:00.000 9
48308 287 2002-12-01 00:00:00.000 10
. . . More

PARTITION BY soporta adem�s otras funciones de agregado incluyendo MIN y MAX.

* Traducci�n de Using Ranking and Windowing Functions in SQL Server 2005 de Stephen Forte publicada en www.sqljunkies.com

Colecciones genéricas en C#

FUNCIONES GENRICAS

Una funcin genrica define un conjunto general de operaciones que se aplicarn a diferentes tipos de datos. Tiene como parmetro el tipo de datos que le son pasados para que opere sobre ellos. Esto nos permite que pueda aplicarse el mismo procedimiento general sobre un amplio rango de datos.

Veamos como funciona con un ejemplo:

public static void intercambia<X>(ref X a,ref X b)

{

X temporal;

temporal = a;

a=b;

b= temporal;

}

public static void Main(string[] args)

{

int i = 10, j=20;

float x=10.1F, y=23.3F;

Console.WriteLine(Original i, j: {0} , {1} \n,i,j);

Console.WriteLine(Original x, y: {0} , {1} \n,x,y);

intercambia(ref i,ref j);

intercambia(ref x,ref y);

Console.WriteLine(Nuevo i, j: {0} , {1} \n,i,j);

Console.WriteLine(Nuevo x, y: {0} , {1} \n,x,y);

string fin = Console.ReadLine();

}

La lnea void intercambia<X>(ref X a,ref X b) le indica al compilador que se est comenzando una definicin genrica. X es el tipo de datos de los valores que sern intercambiados. En main se llama a la funcin intercambia() utilizando dos tipos diferentes de datos: enteros y flotantes. Puesto que es una funcin genrica, el compilador crea automticamente dos versiones de ella una que intercambia valores enteros y otra con valores flotantes.

Podemos definir ms de un tipo genrico siempre que los separemos por comas. Veamos otro ejemplo:

public static void mifunc<tipo1,tipo2>(tipo1 x, tipo2 y)

{

Console.WriteLine( {0} , {1},x,y);

}

public static void Main(string[] args)

{

mifunc(10,hola);

mifunc(0.23,10L);

string fin = Console.ReadLine();

}

Las funciones genricas son equivalentes a las sobrecargadas excepto que son ms restrictivas. Cuando las funciones se sobrecargan, dentro de ellas pueden ocurrir acciones diferentes, pero una genrica debe realizar la misma funcin general para todas las versiones.

Aunque una funcin plantilla puede sobrecargarse a s misma, si es necesario tambin es posible sobrecargarla de forma explcita. En este caso, la funcin sobrecargada redefine (u oculta) la funcin genrica relativa a esa versin especfica. Veamos una versin diferente del primer ejemplo:

public static void intercambia<X>(ref X a,ref X b)

{

X temporal;

temporal = a;

a=b;

b= temporal;

}

// Redefinicin de la versin genrica de intercambia()

public static void intercambia(int a, int b)

{

Console.WriteLine(Esto est dentro de intercambia(int, int)\n);

}

public static void Main(string[] args)

{

int i = 10, j=20;

float x=10.1F, y=23.3F;

Console.WriteLine(Original i, j: {0} , {1} \n,i,j);

Console.WriteLine(Original x, y: {0} , {1} \n,x,y);

intercambia(i,j); // llamada a la versin de intercambia() sobrecargada explcitamente

intercambia(ref x,ref y); // intercambio de floats

Console.WriteLine(Nuevo i, j: {0} , {1} \n,i,j);

Console.WriteLine(Nuevo x, y: {0} , {1} \n,x,y);

string fin = Console.ReadLine();

}

Cuando llamamos a intercambia(i,j) se invoca a la versin sobrecargada explcitamente en el programa. Esta sobrecarga permite confeccionar una versin de una funcin genrica para que se adapte a una situacin especial. Sin embargo, en general, si necesitamos tener diferentes versiones para una funcin para diferentes tipos de datos, deberamos usar funciones sobrecargadas y no plantillas.

CLASES GENRICAS

Adems de las funciones genricas podemos definir una clase genrica, esto es, una clase que define todos los algoritmos usados por ella, pero el tipo de datos que, realmente, va a ser manipulado se especificar como un parmetro cuando se creen los objetos de esa clase.

Son tiles cuando una clase contiene una lgica generalizable. Por ejemplo, el mismo algoritmo que almacena una cola de enteros funciona tambin para una cola de caracteres. Lo mismo podramos decir con una lista enlazada. El compilador generar automticamente el tipo adecuado de objeto en funcin del tipo que se especifique cuando se crea el objeto.

La forma general de la declaracin de una clase genrica se muestra aqu:

public class Generic<T>

<T> es el nombre del tipo de resguardo o parmetro de tipo que se especificar cuando se instancie la clase. Si es necesario, puede definirse ms de un tipo de datos genricos usando una lista separada por comas.

Una vez que se ha creado una clase genrica se puede crear una instancia especfica de esa clase usando la siguiente forma general:

Nombredeclase <Tipo> ob;

Tipo es el nombre del tipo de datos sobre el que trabajar la clase. Los miembros de una clase genrica son, por s mismos, genricos.

La declaracin de una clase genrica es similar a la de una funcin genrica. El tipo real de datos almacenado por la lista, en la declaracin de la clase es genrico.

Una clase plantilla puede tener ms de un tipo genrico de datos (parmetro de tipo). Simplemente hay que declarar todos los tipos de datos requeridos por la clase en una lista separada por comas. Veamos el siguiente ejemplo:

class miclase<Tipo1,Tipo2>

{

Tipo1 i;

Tipo2 j;

public miclase(Tipo1 a, Tipo2 b)

{

i=a;

j=b;

}

public void Mostrar()

{

Console.WriteLine({0} {1}\n,i,j);

}

}

class MainClass

{

public static void Main(string[] args)

{

miclase<int, double> ob1= new miclase<int, double>(10,0.23);

miclase<char, string> ob2= new miclase<char, string>(X,Esto es una prueba);

ob1.Mostrar();

ob2.Mostrar();

string fin= Console.ReadLine();

return;

}

}

}

}

PARMETROS DE TIPO

Cuando una clase genrica es instanciada, lo hace con un parmetro de tipo, enlazando la implementacin de clase incompleta y sin forma con un tipo real, creando una instancia de clase genrica concreta, como se muestra en el siguiente ejemplo:

Cliente<ItemPedido> cliente = new Cliente<ItemPedido>();

Los parmetros de tipo se especifican siempre usando el nuevo operador de genricos <>.

Aunque podemos nombrar nuestros parmetros de tipo como deseemos, por convenio se suele definir la letra T como el nombre estndar para el primer parmetro de tipo. Si nuestra clase requiere parmetros adicionales, el convenio dicta que continuemos con las letras del alfabeto siguientes: U, V Si llegamos a la Z mediante parmetros de tipo, tendramos un problema de diseo tan grande que posiblemente los genricos por s solos no lo podran solucionar.

Para situaciones en las cuales el propsito del parmetro de tipo no es suficientemente explicativo por si mismo, Microsoft recomienda usar un nombre descriptivo para el parmetro de tipo predecido de la T mayscula, como se muestra en el siguiente cdigo:

public class MiGenerico<TClaseAcesoDatos>

Para especificar mltiples parmetros de tipo en una definicin de clase, simplemente debemos separarlas con comas:

public class MiGenerico<T, U, V>

Y para instanciar una clase que requiere mltiples parmetros de tipo, separaremos los tipos con comas como sigue:

MiGenerico<int, string, object> x = new MyGenerics<int, string, object>();

APLICANDO RESTRICCIONES A LOS PARMETROS DE TIPO

Cuando tenemos un parmetro de tipo especificado en nuestra definicin de clase, en realidad hay muy poco que podamos hacer con ello por defecto. Por ejemplo, cmo podemos crear una nueva instancia de ese objeto?. Podramos pensar que el siguiente cdigo debera estar libre de errores de compilacin:

public class Cliente<T>

{

public void AgregarItemDetalle(string nombreItem)

{

T nuevoItem = new T();

items.Add(nuevoItem);

}

}

Desgraciadamente, C# no tiene forma de conocer si el tipo de datos especificado por el parmetro T tiene un constructor por defecto, as que el compilador no permitir que usemos ese tipo en una nueva sentencia sin parmetros.

Para evitar esto, debemos usar restricciones en los parmetros de tipo. Estas restricciones especifican ciertos requerimientos que el parmetro de tipo ha de cumplir. Por ejemplo, podemos especificar que cualquier tipo pasado a nuestra clase genrica deba implementar un constructor (sin parmetros) por defecto.

Todas las restricciones en las clases genricas se indican con la palabra clave where y podemos usar mltiples restricciones en el mismo parmetro separndolas con una coma. Una muestra de cmo podemos especificar una restriccin se puede ver en el siguiente cdigo:

public class Cliente<TItemDetalle> where TItemDetalle : new()

La restriccin precedente indica que el parmetro de tipo especificado por TItemDetalle debe implementar un constructor por defecto. Las siguientes lneas muestran otras formas de especificar restricciones:

public class ListaClientes<T> where T: class, IListaItems
public class Cliente<T,U> where T: new() where U: IDatoCliente

La siguiente tabla muestra todas las restricciones que podemos aplicar a los parmetros de tipo genricos.

Restricciones de Parmetros de tipo Genricos

Restriccin

Descripcin

where T:struct

Indica que T debe ser un tipo valor (excepto Nullable)

where T:class

Indica que T debe ser un tipo referencia, incluyendo cualquier clase, delegado o interface.

where T:new()

Indica que T debe implementar un constructor por defecto. La restriccin new() debe ser especificada la ltima si se usa con varias restricciones en el mismo tipo.

where T: (clase base)

Indica que T debe ser o derivar de la clase base indicada.

where T: (interface)

Indica que T debe ser o implementar el interfaz indicado.

Un patrn de diseo muy utilizado en la OOP es el patrn factory o fbrica. En este patrn, el cdigo solicita nuevas instancias de un tipo dado desde esa fbrica de tipos ms que instanciarlos directamente. Esto da a la fbrica control completo sobre el proceso de instanciacin, adems de la posibilidad de hacer cosas como cachs de tipos preinstanciaos, ejecutar llamadas a Remoting o servicios Web para obtener datos de soporte, y mucho ms.

Para prevenir que la fbrica no llegue a tener demasiadas conversiones de tipos, el patrn ofrece llamadas para el desarrollo de una fbrica nica para cada clase. De esta forma, si tenemos una clase Cliente, siempre tendremos una clase FbricaClientes. Este modelo se usa en sistemas de ORM (Object-Relational Mapping) o en Container-Managed Persistente (CMP).

Para ver como podemos hacer un uso rpido de genricos y restricciones de parmetro de tipo, echemos un vistazo al siguiente cdigo, el cual crea una clase fbrica que puede servir tanto al tipo Cliente como al tipo ClienteEspecial:

public class FabricaClientes<TCliente, U> where TCliente : Cliente, new()
{
   public TCliente ObtenerCliente()
   {
     return new TCliente();
   }
}

El cdigo precedente restringe al tipo TCliente requiriendo que sea o derive del tipo Cliente adems de implementar un constructor por defecto. Ya que el argumento TCliente ha sido restringido con new(), el cdigo puede explcitamente crear una nueva instancia de ese tipo. A travs de la potencia de los genricos, este nuevo ejemplar no es otra cosa que el tipo exacto especificado. No se requiere entonces ninguna conversin de tipos en la clase fbrica, lo cual es un gran beneficio para los desarrolladores OOP.

INTERFACES GENRICOS

Un interface genrico funciona de una forma muy similar a como lo hace una clase genrica. El interface en s mismo acepta uno o ms parmetros de tipo de a misma forma que lo hace una clase genrica. En vez de utilizar el parmetro de tipo en la definicin de la clase, el parmetro de tipo es entonces usado en las sentencias de declaracin de miembros de las que consta el interface.

Un importante beneficio de permitir parmetros de tipo genricos en los interfaces es que las clases que implementan dichos Interfaces no necesitan ejecutar ningn boxing/unboxing innecesarios. Lo mismo podramos decir de las conversiones de tipos. Boxing/unboxing son las operaciones que permiten cambiar entre tipos valor y referencia, y son operaciones costosas. Veamos un ejemplo de interface genrico:

using System;
using System.Collections.Generic;
using System.Text;
 
namespace Generics1
{
  public interface IManejadorDatos<TTipoFila> where TTipoFila : new()
  {
      void AgregarRegistro (TTipoFila registro);
      void BorrarRegistro(TTipoFila registro);
      void ActualizarRegistro(TTipoFila registro);
      void SeleccionarRegistro(TTipoFila registro);
      List<TTipoFila> SeleccionaRegistros();
  }
}

Lo que hace el anterior interface es indicar que cualquier clase que implemente IManejadorDatos<TTipoFila> debe proporcionar los mtodos estndar Agregar, Borrar, Actualizar y Seleccionar tpicamente asociados con objetos de acceso a datos independientemente del tipo de datos subyacente que sea almacenado. Este es un interfaz extremadamente til que nos va a permitir una gran flexibilidad.

 

* Basado en traduccin propia de Visual C# 2005 Unleashed de Kevin Hoffman

Constructor de copia y asignación de objetos en C++

Cuando se pasa un objeto a una funcin, o esta lo devuelve, se pueden presentar dificultades. El modo de evitar estos problemas consiste en definir un constructor de copia. Cuando se pasa un objeto a una funcin, se realiza una copia bit a bit de ese objeto y se guarda en el parmetro de la funcin que recibe el objeto. Sin embargo, hay casos en los que no es deseable la copia exacta del objeto. Por ejemplo, si el objeto contiene un puntero a la memoria asignada, la copia apuntar a la misma memoria que el objeto original. Por consiguiente, si la copia realiza un cambio sobre el contenido de esta memoria, esta ltima tambin cambiar para el objeto original. Cuando la funcin finaliza se destruye la copia mediante una llamada a su destructor, lo cual puede ocasionar efectos colaterales que afectaran al objeto original.

Se produce una situacin parecida cuando una funcin devuelve un objeto. Normalmente el compilador genera un objeto temporal que contiene una copia del valor devuelto por la funcin. ?‰ste desaparece una vez que se devuelve su valor a la rutina que provoc la llamada, mediante una llamada al destructor temporal. Sin embargo, si el destructor elimina alguna informacin necesaria por esa rutina (por ejemplo, liberar memoria asignada dinmicamente) continuarn los problemas.

Estos problemas derivan de la copia bit a bit, de tal forma que para prevenirlos, necesitamos definir de forma exacta que sucede cuando se hace una copia de un objeto de manera que podamos evitar estos efectos colaterales. La forma de llevar a cabo esto es mediante la creacin de un constructor de copia.

C++ define dos tipos distintos de situaciones en las que se da el valor de un objeto a otro. La primera es la asignacin. La segunda, la inicializacin, que puede tener lugar de tres formas:

Cuando se usa un objeto para inicializar otro en una sentencia de declaracin

Cuando se pasa un objeto como parmetro a una funcin

Cuando se crea un objeto temporal para ser usado como el valor devuelto por una funcin

El constructor de copias solo se aplica a la inicializacin, no a la asignacin.

Veamos este ejemplo:

#include <iostream.h>

class info{

private:

int a,b;

public:

void obtener(int *,int *);

info(int dato1,int dato2){

a=dato1;

b=dato2;

};

};

void info::obtener (int *dato1,int *dato2){

*dato1=a;

*dato2=b;

}

int main(){

info i1(1,2);

info i2=i1;

int d1,d2;

i1.obtener(&d1,&d2);

cout << «Atributos i1: a= » << d1 << » b= » << d2 << endl;

i2.obtener(&d1,&d2);

cout << «Atributos i2: a= » << d1 << » b= » << d2 << endl;

return 0;

}

Toda clase contiene un constructor de copia implcito que realiza una copia bit a bit del objeto origen al objeto destino como citamos anteriormente. Es lo que sucede en el ejemplo anterior. Por supuesto, es posible crear un nuevo constructor que oculte al existente por defecto. Para definir un constructor de copia debemos seguir estrictamente la siguiente sintaxis:

nombre_clase::nombre_clase (const nombreclase &)

Supongamos una clase mi_cadena. En ella, el constructor de copia por defecto creara problemas. El puntero a carcter del objeto recin creado apuntara a la misma cadena que el objeto existente. Al borrar cualquiera de los dos se liberara por la cadena, por lo que el puntero del otro objeto apuntar a una zona de memoria disponible para el programa. Cuando se procediese a borrar el segundo objeto, se producira un claro problema. El siguiente cdigo evitara males mayores:

mi_cadena::mi_cadena(const mi_cadena &origen){

clave=origen.clave;

cadena=new char [strlen(origen.cadena)+1];

strcpy(cadena, origen.cadena);

}

Por supuesto, aparte del cdigo anterior, habra que aadir a la parte pblica la siguiente lnea:

mi_cadena (const mi_cadena&);

Tambin es posible hacer una copia de un objeto en otro mediante el siguiente procedimiento:

clase objeto1,objeto2;

objeto2=objeto1;

En este caso, entra en accin el operador «=», que por defecto, tambin hace una copia miembro a miembro. Por fortuna, puede ser sobrecargado para conseguir el comportamiento adecuado. Veamos un ejemplo:

#include <iostream.h>

class miclase{

int a,b;

public:

void asigna(int i,int j){a=i,b=j;}

void muestra(){cout << a << ‘ ‘ << b << endl;}

};

main()

{

miclase o1,o2;

o1.asigna(10,4);

// asigna 01 a 02

o2=o1;

o1.muestra();

o2.muestra();

return 0;

}

Aqu el objeto o1 tiene sus variables miembro a y b fijadas con los valores 10 y 4 respectivamente. A continuacin, o1 se asigna a o2. Esto hace que el valor actual de o1.a se asigne a o2.a y o1.b se asigne a o2.b. Debemos tener en cuenta que una asignacin entre dos objetos simplemente hace que los datos de esos objetos sean idnticos. Los dos objetos estn completamente separados. Por ejemplo, despus de la asignacin, la llamada a o1.muestra() para establecer el valor de o1.a no tiene efecto en o2 o en su valor a.

Slo se pueden usar objetos del mismo tipo en una sentencia de asignacin. Si los objetos no son del mismo tipo, se informa de un error en tiempo de compilacin. No es suficiente, adems, con que los tipos sean fsicamente similares (mismos metodos, variables miembro…) han de tener el mismo nombre de tipo.

Es importante entender que todos los miembros de un objeto se asignan a otro cuando realizamos la copia, incluyendo arrays. Imaginemos un objeto pila s1 al que hemos introducido ya tres elementos; si una vez hecho esto, creamos otros objeto copia de s1, ya contendr esos tres elementos. Si la copia la hacemos antes de introducir los elementos, no los contendr la copia.

#include <iostream>

#define TAM 10

using namespace std;

// Declara una clase pila de caracteres

class pila {

char pil[TAM]; // guarda la pila

int cab; // ndice de la cabeza de la pila

public:

pila();

void push(char ch);

char pop();

};

// Inicializa la pila

pila::pila()

{

cout << «Construyendo una pila\n»;

cab=0;

}

void pila::push(char ch)

{

if (cab==TAM){

cout << «La pila est llena\n»;

return;

}

pil[cab]=ch;

cab++;

}

char pila::pop()

{

if (cab==0){

cout << «La pila est vaca\n»;

return 0;

}

cab–;

return pil[cab];

}

main()

{

pila s1,s2;

int i;

s1.push(‘a’);

s1.push(‘b’);

s1.push(‘c’);

s2=s1; // ahora s1 y s2 son idnticos

for (i=0;i<3;i++) cout << «Saca de s1: » << s1.pop() << endl;

for (i=0;i<3;i++) cout << «Saca de s2: » << s2.pop() << endl;

system(«PAUSE»);

return 0;

}

Debemos tener cuidado al asignar un objeto a otro y asegurarnos de no destruir informacin que pueda necesitarse posteriormente.

Veamos otro ejemplo que ilustra la necesidad de un constructor de copia. Este programa crea un tipo restringido de array de enteros ?seguro??? que previene que se sobrepasen sus lmites. Se asigna el espacio de cada array usando new y dentro de cada objeto array se mantiene un puntero a la memoria.

#include <iostream>

using namespace std;

class matriz {

int *p;

int tam;

public:

matriz(int ta){

p= new int[ta];

if (!p) exit(1);

tam=ta;

cout << «Uso del constructor ‘normal’\n»;

}

~matriz(){delete [] p;}

// constructor de copia

matriz (const matriz &a);

void put(int i, int j){

if (i>=0 && i<tam) p[i]=j;

}

int get(int i){

return p[i];

}

};

/* En el caso siguiente se asigna especificamente memoria

para la copia, y la direccin de esta memoria se asigna a p.

Por tanto, p no est apuntando a la misma memoria asignada

dinmicamente al objeto original */

matriz::matriz(const matriz &a){

int i;

p=new int [a.tam]; // asignacin de memoria para la copia

if (!p) exit(1);

for (i=0; i<a.tam;i++) p[i]=a.p[i]; // contenido de la copia

cout << «Uso del constructor de copia\n»;

}

int main ()

{

matriz num(10); // esta sentencia llama al constructor «normal»

int i;

// colocacin de algunos valores en el array

for (i=0; i<10; i++)

{

num.put(i,i);

}

// presentacin de num

for (i=9;i>=0;i–)

{ cout << num.get(i);}

cout << «\n»;

// creacin de otro array e inicializacin con num

matriz x = num; // esta entencia invoca al constructor de copia

// presentacin de x

for (i=0; i<10; i++)

{ cout << x.get(i);}

cout << «\n»;

system(«PAUSE»);

return 0;

}

Cuando num se usa para inicializar x, se llama al constructor de copia, se asigna memoria para el nuevo array y se almacena en x.p y el contenido de num se copia en el array de x. De esta forma, x y num tienen arrays que contienen los mismos valores, pero cada array es independiente y distinto, es decir, num.p y x.p no apuntan a la misma zona de memoria. Si el constructor de copia no hubiera sido creado, entonces la inicializacin bit a bit matriz x = num habra dado lugar a que los arrays de x y num compartieran la misma memoria.

Como ya dijimos antes, el constructor de copia solo es llamado para las inicializaciones. Por ejemplo, la siguiente secuencia no llama al constructor de copia definido en el ejemplo anterior:

matriz a(10);

matriz b(10);

b=a; // no llama al constructor de copia

En este caso, b=a realiza la operacin de asignacin.

Veamos de nuevo el ejemplo de tipo cadena que nos muestra como el constructor de copia ayuda a prevenir algunos problemas sobrevenidos con el paso de tipos de objetos a funciones, observemos el siguiente programa (incorrecto):

// Este programa tiene un error

#include <iostream>

using namespace std;

class tipocad{

char *p;

public:

tipocad(char *s);

~tipocad(){delete [] p;}

char *obtener() {return p;}

};

tipocad::tipocad(char *s)

{

int l;

l=strlen(s);

p=new char[l];

if (!p){

cout << «Error de asignacin\n»;

exit(1);

}

strcpy(p,s);

}

void mostrar(tipocad x)

{

char *s;

s=x.obtener();

cout << s << endl;

}

int main()

{

tipocad a(«Hola»),b(«mundo»);

mostrar(a);

mostrar(b);

system(«PAUSE»);

return 0;

}

En este programa, cuando un objeto tipocad se pasa a mostrar(), se hace una copia bit a bit y se guarda en el parmetro x. De este modo, cuando finaliza la funcin, x pierde su valor y se elimina. Esto, por supuesto, da lugar a una llamada al destructor de x, que libera x.p. Sin embargo, la memoria que se ha liberado es la misma que todava est siendo utilizada por el objeto empleado para llamar a una funcin. Esto conduce a un error. La solucin consiste en definir un constructor de copia para la clase tipocad que asigne memoria a la copia cuando se cree. Este es el enfoque usado en el siguiente programa corregido:

/* Este programa usa un constructor de copia para permitir que los objetos

tipocad sean pasados a funciones */

#include <iostream>

using namespace std;

class tipocad{

char *p;

public:

tipocad(char *s);

tipocad(const tipocad &o); // constructor de copia

~tipocad(){delete [] p;}

char *obtener() {return p;}

};

tipocad::tipocad(char *s)

{

int l;

l=strlen(s);

p=new char[l];

if (!p){

cout << «Error de asignacin\n»;

exit(1);

}

strcpy(p,s);

}

// Constructor de copia

tipocad::tipocad(const tipocad &o)

{

int l;

l=strlen(o.p);

p=new char[l]; // asigna memoria para la nueva copia

if (!p){

cout << «Error de asignacin\n»;

exit(1);

}

strcpy(p,o.p); // copia de la cadena en la copia

}

void mostrar(tipocad x)

{

char *s;

s=x.obtener();

cout << s << endl;

}

int main()

{

tipocad a(«Hola»), b(» mundo»);

mostrar(a);

mostrar(b);

system(«PAUSE»);

return 0;

}

Ahora, cuando finaliza mostrar() y x pierde su valor, la memoria apuntada por x.p (que se liberar) no es la misma que la usada por el objeto pasado a la funcin.

Vamos a proponer un experimento. Qu sucede si un objeto de una clase derivada se asigna a otro objeto de la misma clase derivada?. Se copia tambin la informacin asociada con la clase base?. La respuesta es s, la informacin de la clase base tambin se copia cuando un objeto de una clase derivada se asigna a otro. El siguiente ejemplo lo demuestra:

#include <iostream>

using namespace std;

class base {

int a;

public:

void carga_a(int n) {a=n;}

int obtiene_a() {return a;}

};

class derivada : public base {

int b;

public:

void carga_b(int n) {b=n;}

int obtiene_b() {return b;}

};

int main()

{

derivada ob1, ob2;

ob1.carga_a(5);

ob1.carga_b(10);

// asigna ob1 a ob2

ob2=ob1;

cout << «Aqu est a y b de ob1: «;

cout << ob1.obtiene_a() << ‘ ‘ << ob1.obtiene_b() << «\n»;

cout << «Aqu est a y b de ob2: «;

cout << ob2.obtiene_a() << ‘ ‘ << ob2.obtiene_b() << «\n»;

// Como es de suponer, la salida es igual para ambos

system(«PAUSE»);

return EXIT_SUCCESS;