Список отдельных типов без преобразования в общий суперкласс - PullRequest
1 голос
/ 09 июля 2020

Как сохранить объекты в списке, сохранив их исходный тип? Без преобразования в общий суперкласс.

Чтобы приведенный ниже код мог работать:

using System;
using System.Collections.Generic;

public class Test
{
    public static void Main(string[] args)
    {
        var list = new List<Super>()
        {
            new Type1 { Number = 1, Info = "infomatin" },
            new Type2 { Number = 2, Prop = "propty" }
        };
        foreach (var t in list)
        {
            Doer.Do(t);
        }
    }
}

public class Super
{
    public int Number { get; set; }
}

public class Type1 : Super
{
    public string Info { get; set; }
}

public class Type2 : Super
{
    public string Prop { get; set; }
}

public static class Doer
{
    public static void Do(Type1 arg)
    {
        Console.WriteLine($"Got type 1 with {arg.Info}");
    }

    public static void Do(Type2 arg)
    {
        Console.WriteLine($"Got type 2 with {arg.Prop}");
    }
}

Требуемый вывод:

Got type 1 with infomatin
Got type 2 with propty

Фактический вывод, ошибка компилятора:

Test.cs(15,21): error CS1503: Argument 1: cannot convert from 'Super' to 'Type1'

Я мог бы сделать это внутри foreach

if (t instanceof Type1)
    Doer.Do((Type1) t);
else if (t instanceof Type2)
    Doer.Do((Type2) t);

Но я не хочу писать так много кода. Тем более, что я добавляю больше подклассов Super.

, я бы хотел добавить к моему Doer только дополнительный метод для обработки нового типа, тогда обо всем остальном позаботятся.

Ответы [ 2 ]

3 голосов
/ 10 июля 2020

Вы можете использовать сопоставление с образцом только с одним Do() методом

public static void Do(Super arg)
{
    switch(arg)
    {
        case Type1 t1:
            Console.WriteLine($"Got type 1 with {t1.Info}");
            break;
        case Type2 t2:
            Console.WriteLine($"Got type 2 with {t2.Prop}");
            break;
        default:
            throw new NotSupportedException();
    }
}

Что касается исключения приведения , я думаю, вы неправильно поняли, как работает C#. Когда вы сохраняете в базовом классе, содержимое памяти по-прежнему принадлежит производному классу, и при этом не выполняется приведение в смысле копирования данных из одного типа в другой

Этот тип операции не включает копирование данных

Super obj = new Type1();

Это тоже не

Type1 t1 = (Type1)obj;

Это просто ссылка t1, накладывающая другую «маску», чем obj, и данные за маской такие же.

Edit 1

Любое приведение (в форме (type)value в C#) может включать преобразование или нет. Некоторые примеры, где obj - это тип object, sup - тип class Super, t1 - тип class Type1 : Super и t2 - тип class Type2 : Super.

  • Без преобразования или идентичности
    • obj = sup;
    • sup = t1;
    • sup = t2;
    • sup = t1; t1 = (Type1)sup;
    • obj = t1; sup = (Super)obj;
    • obj = t1; sup = (Type1)t1;

Приведенный ниже код требует, чтобы следующий пользовательский код преобразования был добавлен к Type1 и Type2 соответственно.

public static implicit operator Type1(Type2 t2) => new Type1() { Info = t2.Prop };
public static explicit operator Type2(Type1 t1) => t1.Info.StartsWith("prop") ? new Type2() { Prop = t1.Info } : throw new NotSupportedExpection();
  • Приведения неявного преобразования (копирование данных, может завершиться сбоем)

     {   // Implicit conversion Type2 -> Type1
         object obj = new Type2() { ID = 2, Prop = "propval" };
         Type1 t1 = (Type2)obj;
     }
    
  • Приведения явного преобразования (копирование данных, может завершиться сбоем)

     {
         // Explicit conversion Type1 -> Type2
         object obj = new Type1() { ID = 1, Info = "propInfo" };
         Type2 t2 = (Type2)(Type1)obj;
     }
    

Читать https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/conversions. для более точной информации.

2 голосов
/ 10 июля 2020

Если вы обнаружите, что используете переключатель / футляр и литье, велика вероятность, что вы делаете это неправильно. При правильно спроектированной объектной модели в этом нет необходимости.

Например,

abstract public class Super
{
    public int Number { get; set; }
    public abstract void Do();
}

public class Type1 : Super
{
    public string Info { get; set; }

    public override void Do()    
    {
        Console.WriteLine($"Got type 1 with {this.Info}");
    }
}

public class Type2 : Super
{
    public string Prop { get; set; }

    public override void Do()    
    {
        Console.WriteLine($"Got type 2 with {this.Prop}");
    }
}

Теперь вы можете просто сделать это в своем l oop:

public static void Main(string[] args)
{
    var list = new List<Super>()
    {
        new Type1 { Number = 1, Info = "infomatin" },
        new Type2 { Number = 2, Prop = "propty" }
    };
    foreach (var t in list)
    {
        t.Do();
    }
}

Вышеизложенное соответствует Говори, не спрашивай , который является традиционной объектно-ориентированной философией.

Если вы беспокоитесь о разделении проблем (например, вы не Я хочу, чтобы ваши классы знали "Консоль"), тогда вы можете добавить внешнюю функциональность:

abstract public class Super
{
    public int Number { get; set; }
    public abstract void Do(Action<int> action);
}

public class Type1 : Super
{
    public string Info { get; set; }

    public override void Do(Action<int> action)    
    {
        action(this.Info);
    }
}

public class Type2 : Super
{
    public string Prop { get; set; }

    public override void Do(Action<int> action)    
    {
        action(this.Prop);
    }
}

public static void Main(string[] args)
{
    var list = new List<Super>()
    {
        new Type1 { Number = 1, Info = "infomatin" },
        new Type2 { Number = 2, Prop = "propty" }
    };
    foreach (var t in list)
    {
        t.Do( x => Console.WriteLine("The value that we're interested in is {0}", x));
    }
}

Есть еще одна ситуация, которая может иметь место здесь (на основе ваших комментариев). Допустим, у вас есть «чистые» объекты DTO, у которых нет методов, и вы не хотите добавлять их по какой-либо причине, например, возможно, DTO генерируются кодом, и вы не можете их изменять. На самом деле это обычная ситуация (мне тоже нравятся бездетные DTO).

Чтобы сделать ситуацию более реальной, давайте воспользуемся более значимыми примерами. Предположим, у вас есть множество объектов, которые могут содержать имя конечного пользователя, но с различными идентификаторами:

abstract public class Super
{
}

public class Type1 : Super
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Type2 : Super
{
    public string FullName { get; set; }
}

Здесь возникает соблазн написать такой случай переключателя:

foreach (var t in list)
{
    switch (t)
    {
         case Type1 type1 : Console.WriteLine("Name is {0} {1}", type1.FirstName, type1.LastName);
         case Type2 type2: Console.WriteLine("Name is {0}", type2.FullName);
         default:
             throw new InvalidOperationException();
    }
}

Проблема здесь в том, что время выполнения throw будет происходить каждый раз, когда кто-то добавляет другой тип объекта, но не забывает обновить ваш оператор switch. Это может не быть проблемой, но это также может быть огромной проблемой, например, если ваши DTO хранятся в отдельной библиотеке от вашего процессора Do, и вам не нужно обновлять оба одновременно (что может быть проблема развертывания в определенных архитектурах).

Что здесь отсутствует, так это бизнес-концепция «Имя» агности c того, откуда оно взялось. Где-то какой-то код должен преобразовать эти различные объекты во что-то, что имеет имя, и желательно, чтобы logi c был где-то инкапсулирован.

Вот где я бы использовал класс адаптера.

class NameHolder
{
    public string FullName { get; }

    public NameHolder(Type1 type1)
    {
        this.FullName = type1.FirstName + " " + type1.LastName;
    }

    public NameHolder(Type2 type2)
    {
        this.FullName = type2.FullName;
    }
}

С добавлением этой недостающей бизнес-концепции логи c становятся очень простыми:

public static void Main(string[] args)
{
    var list = new List<NameHolder>()
    {
        new NameHolder(new Type1 { Number = 1, Info = "infomatin" }),
        new NameHolder(new Type2 { Number = 2, Prop = "propty" })
    };
    foreach (var t in list)
    {
        Do(t.FullName);
    }
}

Обратите внимание на отсутствие throw. Преимущество этого подхода в том, что все типы разрешаются во время компиляции, поэтому, если вы забудете добавить logi c для сопоставления правильных полей, вы получите ошибку времени компиляции, которую вы можете обнаружить и немедленно исправить.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...