EDITAR: Este no es el mejor enfoque para este problema. Logré cambiar el diseño y usar una Lista / Diccionario que crea una nueva instancia de una clase dependiendo de la clave seleccionada. De esta manera, no necesita ningún reparto ni Reflexión ni declaración de cambio.

Tengo una pregunta que intenté resolver por mí mismo pero no estoy satisfecho con mis soluciones. Pongamos un pequeño ejemplo: tengo una clase de granja, una clase de molino y una clase de panadería. Esos son edificios, y me gustaría almacenarlos en una lista y, obviamente, me gustaría administrar sus elementos cada vez que lo necesite. En este caso, este es un código del lado del servidor, y la lista necesitaría tener una lista de todas las construcciones de un jugador para que pueda enviarle esa lista siempre que lo solicite, por ejemplo, cuando se une. El jugador podría construir una Granja para comenzar a cultivar algo de trigo, luego un Molino para comenzar a aplastarlo para hacer harina. Luego construye una panadería para hacer pan o pasteles. Este es el código de ejemplo:

public class Program
{
    public List<> Buildings = new List<>() //Or whatever solution to store them
}

public class Farm
{
    public string Name;
    public int CropType;
    public float Timer;
}

public class Mill
{
    public string Name;
    public float Timer;
}

public class Bakery
{
    public string Name;
    public int ProductionType;
    public bool IsOpen;
    public float Timer;
}

He intentado usar interfaces, clases abstractas y clases normales para obtener, pero no he encontrado mi solución.

Este es mi intento:

public interface IBuilding
    {
        string ExposedName { get; set; }
        string ExposedDescription { get; set; }
    }

    public class Farm : IBuilding
    {
        public string Name { get { return Name; } set { Name = value; } }
        public string Description { get { return Description; } set { Description = value; } }
        public int Crop;
    }

    public class Mill : IBuilding
    {
        public string Name { get { return Name; } set { Name = value; } }
        public string Description { get { return Description; } set { Description = value; } }
        public float Timer;
    }

Me gustaría poder acceder a cada variable de cada elemento de la Lista o cualquier solución que me sugiera sin usar código no funcional.

c#
1
Penca53 9 may. 2019 a las 18:33

4 respuestas

La mejor respuesta

Heredar de una clase base es la forma correcta de hacer esto. Pero al acceder a elementos de la lista, solo debe usar la interfaz de la clase base . Eso significa que solo los miembros de Building pueden usar. Cualquier cosa que dependa de la verificación del tipo de tiempo de ejecución funcionará en su contra cuando agregue nuevos edificios, ya que debe recordar cada lugar donde debe agregar un nuevo caso en el interruptor.

Extraiga a la clase base solo aquellos miembros a los que se pueda llamar en cualquier edificio e impleméntelos en clases derivadas.

Usted mencionó que desea no solo almacenarlos en una lista, sino también enviarlos al cliente (y probablemente recibirlos). Así que recomiendo definir Building como este:

public abstract class Building : ISerializable, IDeserializable {
    // here you define interface, members which every building has
    public abstract string Name { get; }
    public abstract short Id { get; }
    public abstract void Update (); // do staff that this type of building does

    public abstract void ReadMembers (BinaryReader reader);
    public void Write (BinaryWriter writer) {
        writer.Write (Id);
        WriteMembers (writer);
    }
    public abstract void WriteMembers (BinaryWriter writer);
}

Entonces, cuando iteras a través de una lista, llamas a métodos de la clase base:

foreach (var b in buildings) b.Update ();

foreach (var b in buildings) b.Write (writer);

Leer es complicado. Requiere escribir un método de extensión para su lector, que debe conocer los identificadores para crear el tipo correcto de edificio.

public static class BinaryReaderExtension {

    private static Dictionary <short, Func <Building>> generators = new Dictionary <short, Func <Building>> ();


    static BinaryReaderExtension () {
        generators [0] = () => new Farm ();
        generators [1] = () => new Mill ();
    }


    public static Building ReadBuilding (this BinaryReader reader) {
        var b = generators [reader.ReadInt16 ()] ();
        b.ReadMembers (reader);
        return b;
    }

}

Importante: Id debe devolver un número único para cada edificio, igual que en la extensión.

Hay mucho espacio para experimentar.
Puede usar una matriz en lugar de un diccionario. Puede escribir una extensión para escritor y eliminar Id y Write de la clase Building. Puede usar el método estático en Building devolviendo Building en lugar de la extensión del lector. También puedes hacer esto en la clase Building:

private Dictionary <Type, short> codeByType;
private Dictionary <short, Func <Building>> instanceByCode;
private void RegisterBuilding (Type t, short code) {
    // custom error checking for code to be unique, etc
}

Y llenar diccionarios desde Building inicializador estático a través de esta función.

La clase derivada debe definirse como:

public class Farm : Building {
    public override string Name => "Farm"; // assuming c# 6.0
    public override short Id => 0;

    private Crop crop;
    // other farm-specific things
    public override void Update {
        // you access a private field,
        // but Update method itself can be called even if you don't know the type
        // every building has its own implementation
        crop.Grow ();

        // other farm-specific things
    }

// override other methods, feel free to access private members of Farm here
}
0
trollingchar 9 may. 2019 a las 19:47

Según su intento, pero con una clase abstracta:

public abstract class Building
{
    string Name { get; set; }
    string Description { get; set; }
}

public class Farm : Building
{
    public int Crop { get; set; }
}

public class Mill : Building
{
    public float Timer { get; set; }
}

Otra parte de la solución podría ser las posibilidades IEnumerable (como OfType) para acceder a sus propiedades de los elementos en su lista en un bucle. (Sin el código "manual" ni el código ulgy "isXXX")

class MyCollection
{
    List<Building> buildings;

    public MyCollection()
    {
        buildings = new List<Building>();

        buildings.Add(new Farm() { Crop = 4 });
        buildings.Add(new Mill() { Timer = 4.5f });
        buildings.Add(new Farm() { Crop = 5 });
        buildings.Add(new Farm() { Crop = 6 });
        buildings.Add(new Mill() { Timer = 42 });
        buildings.Add(new Farm() { Crop = 55 });
    }

    public void Print()
    {
        foreach (Farm f in buildings.OfType<Farm>())
        {
            Console.WriteLine(f.Crop);
        }

        foreach (Mill m in buildings.OfType<Mill>())
        {
            Console.WriteLine(m.Timer);
        }
    }
}

No sé si es lo que quieres decir con

"Me gustaría poder acceder a cada variable de cada elemento de la Lista o cualquier solución que me sugieran sin usar código no funcional".

2
KiwiJaune 9 may. 2019 a las 15:57

En esta descripcion:

El jugador podría construir una Granja para comenzar a cultivar algo de trigo, luego un Molino para comenzar a aplastarlo para hacer harina. Luego construye una panadería para hacer pan o pasteles.

... usted describe tres usos diferentes para tres clases diferentes.

Para almacenar un Farm, un Mill y un Bakery en una lista, tendrían que tener algún tipo común. Pero eso realmente no viene al caso. Son tres clases diferentes que hacen cosas diferentes, por lo que si las usa para cultivar, moler y hornear, no tiene ningún beneficio ponerlas en una lista entre ellas.

Una interfaz o clase común solo es útil para describir qué objetos tienen o hacen en común y usarlos en ese contexto.

Si envía a un cliente una lista de Building, el cliente solo sabe que tiene descripciones y nombres. No pueden conocer los comportamientos únicos de tipos diferentes y más específicos de Building. Ellos no deberían saberlo. De esa manera si Building tuviera un método como:

someBuilding.CloseDoors()

Entonces sería posible llamar a ese método en un edificio sin saber si es una panadería, un molino, etc. No nos importa, solo queremos cerrar las puertas. Pero si queremos Bake entonces no queremos un Building. Queremos un Bakery.

Si desea dar al cliente objetos que hagan cosas claramente diferentes, entonces una lista de un tipo de cosas no logrará eso.

Los detalles de cómo proporcionar esta funcionalidad dependen de su aplicación. Pero aquí hay algunas alternativas:

  • No proporcione una lista de algo común. Proporcionar instancias o listas de algo específico. ¿Quieres hornear? Aquí hay una lista de sus panaderías. (Las propiedades comunes Name y Description podrían facilitar la visualización de una lista de edificios de forma reutilizable). Elija uno y úselo.
  • ¿El cliente realmente necesita una lista de panaderías, o solo una? Quizás todo lo que necesitan es un método API Bake, y las panaderías disponibles se administran detrás de escena.
2
Scott Hannen 9 may. 2019 a las 16:37

Así es como lo configuraría. Necesitas heredar del edificio, como otros han mencionado. Al ejecutar el ciclo, solo verifique si el edificio es una granja o una panadería.

public abstract class Building
{
    public string Name { get; set; }
    public float Timer { get; set; }


}
public class Farm : Building
{
    public int CropType { get; set; }
    public Farm()
    {
    }
    public Farm(string name, int cropType, float timer)
    {
        Name = name;
        Timer = timer;
        CropType = cropType;
    }
}
public class Bakery : Building
{
    public int ProductionType { get; set; }
    public bool IsOpen { get; set; }
    public Bakery()
    {
    }
    public Bakery(string name, float timer, int prodType, bool isOpen)
    {
        Name = name;
        Timer = timer;
        ProductionType = prodType;
        IsOpen = isOpen;
    }
}

public class Example
{
    public static void myMethod()
    {
        Bakery bakery1 = new Bakery("Bake1", 123, 1, true);
        Farm farm1 = new Farm("Farm1", 2, 321);

        List<Building> buildings = new List<Building>();
        buildings.Add(bakery1);
        buildings.Add(farm1);

        foreach(Building building in buildings)
        {
            if(building is Farm)
            {
                Farm f1 = (Farm)building;
                int cropType = f1.CropType;
            }
            else if(building is Bakery)
            {
                Bakery b1 = (Bakery)building;
                int prodType = b1.ProductionType;
            }
        }
    }
}
-2
kristech 9 may. 2019 a las 16:42