2010-11-30 9 views
5

Bien, entonces tengo una clase base a la que llamaremos TFruit. De esto hay varios descendientes como TApple, TOrange y así sucesivamente. Necesito guardar las propiedades de las clases descendientes en un archivo.Necesidad de asociar un valor entero único con las clases

Para poder crear la clase correcta al cargar los datos, cada clase necesita tener un ID que escriba en el archivo antes de escribir los datos reales. Actualmente, se me ha ocurrido con la siguiente forma de hacerlo:

type 
    TFruit = class 
    const ID = 0; 
    end; 

    TApple = class(TFruit) 
    const ID = 1; 
    end; 

    TOrange = class(TFruit) 
    const ID = 2; 
    end; 

Prueba esto, descubrí que tengo que ser super cuidado qué clase declaro. Si uso esto:

var Fruit: TFruit; 

    Fruit := TOrange.Create; 

... entonces Fruit.ID volverá cero. Sin embargo, declarando Fruit como TOrange producirá el resultado esperado Fruit.ID = 2 (Alguien sabe por qué?)

Así que, básicamente, estoy haciendo esto correcto o hay una mejor manera de hacerlo? Tener que crear una función de clase y devolver un valor desde allí parece muy feo en comparación (declaración de función adicional, implementación y código).

Respuesta

5

Una solución más fácil de mantener sería crear una clase de asignación donde registrar todas las clases que desea convertir en un número entero.

Ventajas

  • capacidad de detectar registros duplicados.
  • Independiente de su estructura de clase.
  • Incluye la transformación a un nombre de clase.

Uso

RegisterClass.Register(0, TFruit); 
    RegisterClass.Register(1, TApple); 
    RegisterClass.Register(2, TOrange); 

Implementación

TRegisterClass = class 
    private 
    FList: TStringList; 
    public 
    function FindID(AClass: TClass): Integer; 
    function FindClassName(const ID: Integer): string; 
    procedure Register(const ID: Integer; AClass: TClass); 
    end; 
    ... 
    function TRegisterClass.FindID(AClass: TClass): Integer; 
    begin 
    Assert(Assigned(AClass)); 

    Result := -1; 
    if FList.IndexOf(AClass.ClassName) <> -1 then 
     Result := Integer(FList.Objects[FList.IndexOf(AClass.ClassName)]); 
    end; 

    function TRegisterClass.FindClassName(const ID: Integer): string; 
    var 
    I: Integer; 
    begin 
    Result := EmptyStr; 
    for I := 0 to Pred(FList.Count) do 
     if Integer(FList.Objects[I]) = ID then 
     begin 
     Result := FList[I]; 
     Exit; 
     end; 
    end; 

    procedure TRegisterClass.Register(const ID: Integer; AClass: TClass); 
    begin 
    if IsAlreadyRegistered(ID) then 
     raise Exception.Create('Duplicate ID Registration') 
    else if IsAlreadyRegistered(AClass) then 
     raise Exception.Create('Duplicate Class Registration'); 

    FList.AddObject(AClass.ClassName, Pointer(ID)); 
    end; 

Tenga en cuenta que hay mejores estructuras para asignar una cadena en un entero. Al escribir esto sin un compilador y sin conocer muchas estructuras básicas más allá de Delphi5, he elegido una implementación obvia.

Tenga en cuenta que las funciones sobrecargadas IsAlreadyRegistered todavía tienen que ser escrito

+0

Para mayor comodidad, se podría agregar un método de clase a la clase base de objetos registrados que devuelven el ID de esa clase. Y dado que los métodos de clase (a diferencia de los métodos estáticos de estilo C#) reciben el tipo de clase como parámetro 'self', esto funciona como se esperaba. – CodesInChaos

+0

@CodeInChaos, cierto, pero eso requeriría que cada clase "supiera" su identificación que se puede evitar y podría convertirse en un dolor crear nuevos descendientes para tratar de averiguar cuál es la próxima identificación disponible. –

+0

No, quiero decir que este método llama a su función 'FindID' con' self' como param. Pero podría ser mejor desde el punto de vista arquitectónico si las propias clases no conocen los ID, ya que pueden considerarse un detalle de implementación del serializador. – CodesInChaos

1

En primer lugar, es necesario

type 
    TFruit = class 
    end; 

    TApple = class(TFruit) 
    end; 

    TOrange = class(TFruit) 
    end; 

y luego se puede utilizar Fruit.ClassName y Fruit.ClassType, no puede usted?

function ClassToID(const Fruit: TFruit): word; 
begin 
    if Fruit is TApple then 
    result := 1 
    else if Fruit is TOrange then 
    result := 2; 
end; 

o

TFruitClass = class of TFruit; 

type 
    TFruitAndID = record 
    FruitClass: TFruitClass; 
    ID: word; 
    end; 

const FruitIDs: array[0..1] of TFruitAndID = 
    ((FruitClass: TApple; ID: 1), (FruitClass: TOrange; ID: 2)); 

function ClassToID(Fruit: TFruit): word; 
var 
    i: Integer; 
begin 
    for i := 0 to high(FruitIDs) do 
    if FruitIDs[i].FruitClass = Fruit.ClassType then 
     Exit(FruitIDs[i].ID); 
end; 
+0

No. Necesito guardar cientos de miles de "frutas" en un archivo y por razones de espacio no quiero usar el nombre de la clase. Necesito un número que escribiré como campo de Word (16 bits) en el archivo. – David

+0

@David: puede escribir una función 'ClassToID'. –

1

Si está utilizando Delphi 2010 puede utilizar attributes para etiquetar sus clases con el ID.

+0

Tristemente estoy usando Delphi 2005 para este proyecto. Si fuera D2010 probablemente iría con tu sugerencia. – David

3

hay muchas posibilidades, por ejemplo:

function TFruit.GetClassId(): Word; 
begin 
    Result := CRC16(ClassName); 
end; 
+0

Esto no conduce a ID únicos. – CodesInChaos

+0

Colisión en tales cuerdas cortas? Casi imposible. –

2

Alguien sabe por qué?

Porque está declarando un campo de clase? TOrange hereda de TFruit, por lo que también tiene el campo ID = 0. Luego lo reemplaza con otro ID = 2 campo. Ahora tienes dos de estos. Si lanzas TOrange a TFruit, obtendrás un campo heredado, esta es precisamente la forma de acceder a ellos.

Si estás en Delphi 2010+, los atributos de uso:

[ClassId(4)] TOrange = class(TFruit) 

Pero, ¿por qué necesita estos identificadores en el primer lugar? Tendrá que marcar manualmente cada tipo de clase, esto es propenso a errores. Solo usa el nombre de la clase.

var t: TOrange; 
begin 
    writeFile(t.Classname, t.Data); 

Si está tan preocupado con el espacio, mantener una tabla nombre de clase-id en el principio del archivo y asignar identificadores de forma dinámica a medida que avanza:

procedure WriteObject(c: TObject); 
var id: integer; 
begin 
    if not GetAlreadyRegisteredClassnameId(c.Classname, id) then 
    id := AddClassnameToTable(c.Classname); 

    writeToCache(id, c.Data) 
end; 

procedure WriteFile() 
var i: integer; 
begin 
    for i := 0 to ObjectCount-1 do 
    WriteObject(objects[i]); 
    OutputClassnameTableToFile; 
    OutputObjectCacheToFile; 
end; 

(Por supuesto, haciendo caso omiso de las limitaciones de memoria aquí para fines demostrativos, pero es fácil hacerlo sin memoria caché también)

0

Mirando en otro ángulo: ¿por qué ID no es una propiedad de objeto de solo lectura (en lugar de una clase const)?

Así:

type 
    TFruit = class 
    protected 
    FId: Integer; 
    published 
    property ID:Integer read FId; 
    end; 

    TApple = class(TFruit) 
    constructor Create; 
    end; 

    TOrange = class(TFruit) 
    constructor Create; 
    end; 

<...> 
constructor TApple.Create; 
begin 
    FId := 1; 
end; 

constructor TOrange.Create; 
begin 
    FId := 2; 
end; 

Por lo tanto, el código de ejemplo funcionará ahora. (Los descendientes pueden ver FId porque es un campo protegido). EDIT: cambia la visibilidad desde pública a publicada. Pero lo mismo se puede lograr usando la directiva $ RTTI para permitir RTTI a los miembros públicos.

+0

Para que RTTI funcione, iirc la propiedad necesita tener visibilidad 'publicada' (el código de ejemplo en los usos de la pregunta está implícitamente publicado) – mjn

+0

@mjustin - esto depende de que la versión Delphi 2010+ haya extendido RTTI –

+0

@Gerry: el código es obligatorio para trabajar con Delphi 2005 (como he visto en un comentario), pero tienes razón :) – mjn

Cuestiones relacionadas