C# Как правильно возвратить базовый класс при ошибке в создании производного класса

Пусть есть следующая структура классов:

class Person
{        
    public string Name { get; set; }        
    public Person(string name)
    {
        Name = name;
    }
}
class Employee : Person
{
    public string Company { get; set; }
    public Employee(string name, string company): base(name)
    {
        Company = company;
    }
}

class Student : Person
{
    public string Group { get; set; }
    public Student(string name, string group) : base(name)
    {
        Group = group;
    }
}

class Developer : Person
{
    public string Language { get; set; }
    public Developer(string name, string language) : base(name)
    {
        Language = language;
    }
}

Решение о создании того или иного экземпляра принимается в классе фабрике:

static class Factory
{
    static Person GetPerson(byte[] bytes, string name, string seconParam)
    {
        switch (bytes[0])
        {
            case 0: return new Employee(name, seconParam);
            case 1: return new Student(name, seconParam);
            case 2: return new Developer(name, seconParam);
            default: return new Person(name);
        }
    }
}

Но при возникновении ошибки при создании конкретного экземпляра я ничего не получаю. (этот пример сильно упрощён) Хотелось бы при возникновении ошибки при создании производного класса получить базовый. На ум пришло такое решение:

static class Factory2
{
    static Person GetPerson(byte[] bytes, string name, string seconParam)
    {
        switch (bytes[0])
        {
            case 0:
                {
                    try
                    {
                        return new Employee(name, seconParam);
                    }
                    catch
                    {
                        return new Person(name);
                    }
                }
            case 1:
                {
                    try
                    {
                        return new Student(name, seconParam);
                    }
                    catch
                    {
                        return new Person(name);
                    }
                }
            case 2:
                {
                    try
                    {
                        return new Developer(name, seconParam);
                    }
                    catch
                    {
                        return new Person(name);
                    }
                }
            default: return new Person(name);
        }
    }
}

Можно ли как-то это сделать проще и без повторов кода?

UPDATE1: Это реальные классы дескрипторов :

public class Descriptor
{

    public byte DescriptorTag { get; internal set; }

    public byte DescriptorLength { get; internal set; }
    public byte[] Data { get; internal set; }


    public virtual string Name => DescriptorDictionaries.DescriptorNames[DescriptorTag];
    public override string ToString()
    {
        return $"Tag: 0x{DescriptorTag:X2}, {Name}, Length: {DescriptorLength}";
    }

    public Descriptor(ReadOnlySpan<byte> bytes)
    {
        int pointer = 0;
        DescriptorTag = bytes[pointer++];            
        DescriptorLength = bytes[pointer++];            
        if (bytes.Length - pointer >= DescriptorLength)
        {
            Data = bytes.Slice(pointer - 2, DescriptorLength + 2).ToArray();
        }
        else
        {
            Logger.Send(LogStatus.Warning, $"Pointer out of descriptor");
        }
    }
    
}

public class BouquetNameDescriptor : Descriptor
{
    public string BouquetName { get; }
    public BouquetNameDescriptor(ReadOnlySpan<byte> bytes) : base(bytes)
    {
        BouquetName = Utils.BytesToString(bytes.Slice(2, DescriptorLength));
    }
    public override string ToString()
    {
        return $"Bouquet name: {BouquetName}";
    }
}
public class LogicalChannelNumber : Descriptor
{
    public class LcnItem
    {
        public ushort ServiceID { get; internal set; }
        public bool VisisbleServiceDlag { get; internal set; }
        public ushort LogicalChannelNumber { get; internal set; }
    }

    public List<LcnItem> LcnItems { get; internal set; }
    public LogicalChannelNumber(ReadOnlySpan<byte> bytes) : base(bytes)
    {
        var pointer = 2;
        LcnItems = new List<LcnItem>();
        while (pointer < DescriptorLength)
        {
            var item = new LcnItem();
            item.ServiceID = (ushort)((bytes[pointer++] << 8) + bytes[pointer++]);                
            item.VisisbleServiceDlag = ((bytes[pointer] & 0x80) >> 7) != 0;
            item.LogicalChannelNumber = (ushort)(((bytes[pointer++] & 0x03) << 8) + bytes[pointer++]);                
            LcnItems.Add(item);
        }
    }

    public override string ToString()
    {
        string str = "";
        foreach (var item in LcnItems)
        {
            str += $"Service id:{item.ServiceID}, visible:{item.VisisbleServiceDlag}, lcn: {item.LogicalChannelNumber} \n";
        }
        return str;
    }
}

Дескрипторы могут быть разной длины. Длину можно узнать только из самого дескриптора. Если в какой-то момент пришёл User defined дескриптор у которого неизвестна его структура и он оказался у примеру очень короткий ( а у нас уже определён дескриптор с таким тегом) , то мы словим исключение при попытке обратится к несуществующему элементу массива или что-то подобное. В таком случае нам нужно извлечь из входных данных базовый дескриптор у которого есть всего 2 поля, но они 100% есть, так как прописаны в стандарте. По длине дескриптора мы верно определим начало следующего дескриптора. Собственно сможем разобрать всю таблицу корректно.


Ответы (1 шт):

Автор решения: aepot

Очень короткий ответ:

static class Factory
{
    static Person GetPerson(byte[] bytes, string name, string seconParam)
    {
        try
        {
            switch (bytes[0])
            {
                case 0: return new Employee(name, seconParam);
                case 1: return new Student(name, seconParam);
                case 2: return new Developer(name, seconParam);
                default: return new Person(name);
            }
        }
        catch
        {
            return new Person(name);
        }
    }
}

Я по-всякому пытался подъехать к вашему решению, но мне не хватило информации или некоторые способы сильно влияли на процесс десереализации данных.

В частности unsafe struct размером 256 байт и ref readonly struct, из минусов - нет наследования, из плюсов, один и тот же участок памяти можно переиспользовать на всем процессе десереализации (менять тип стурктуры "на лету"), что сильно ускорит процесс считывания данных. Так же, структуры константного размера (256 байт) можно собрать в пул и переиспользовать, или вообще увести часть аллокаций в стек, то есть свести выделения памяти при чтении потока данных к нулю. От прелестей ООП конечно при такой работе с памятью пришлось бы частично отказаться. Но это уже другая длинная история. И если вас полностью устраивает текущая производительность приложения - то этим всем можно пока пренебречь. Идею можно подсмотреть здесь.


Есть несколько советов.

public class LcnItem

Вынесите за пределы класса дескриптора, не создавайте публичные вложенные типы. Если вам действительно нужен тип только для использования внутри какого-то конкретного класса, вложите и сделайте его приватным.

Ну и конструктор LogicalChannelNumber я бы как-то так реализовал

public LcnItem[] LcnItems { get; }

public LogicalChannelsDescriptor(byte[] bytes) : base(bytes)
{
    LcnItems = new LcnItem[DescriptorLength / 4];
    for (int i = 0; i < LcnItems.Length; i++)
    {
        ReadOnlySpan<byte> span = bytes.AsSpan()[(2 + i * 4)..];
        LcnItems[i] = new LcnItem()
        {
            ServiceID = BinaryPrimitives.ReadUInt16LittleEndian(span),
            VisisbleServiceDlag = (span[2] & 0x80) != 0,
            LogicalChannelNumber = (ushort)(BinaryPrimitives.ReadUInt16LittleEndian(span[2..]) & 0x03ff)
        };
    }
}

Не стоит использовать список там, где заранее известно количество элементов.

internal set для свойств, которые назначаются в конструкторе, вам тоже ни к чему. Если же вам нужны свойства базового класса, которые могут быть изменены только в наследниках, используйте protected, а не internal.

И последнее, вот есть у вас класс Descriptor, и у него есть DescriptorLength. Не стоит повторять идно и то же слово при обращении к переменной, ведь если вы переименуете в Length, то вместо descriptor.DescriptorLength получится descriptor.Length - коротко и ясно. Так код будет легче читать.

→ Ссылка