Есть ли способ предотвратить / обнаружить автоматическое / неявное преобразование типов? - PullRequest
2 голосов
/ 13 июля 2020

Рассмотрим эти методы, каждый извлекает байты из входного буфера:

byte ReadByte(List<byte> data); //removes and returns 1 byte
UInt16 ReadUInt16(List<byte> data); //removes and returns 2 bytes as a short
UInt32 ReadUInt32(List<byte> data); //removes and returns 4 bytes as an int

Теперь у меня есть структура / класс вроде:

class Thing
{
 public byte a{get;set;}
 public UInt16 b{get;set;}
 public UInt32 c{get;set;}
 public byte d{get;set;}

 public void Read(List<byte> data)
 {
  a = ReadByte(data);
  b = ReadUInt16(data);
  c = ReadUInt16(data); //BUG this is a 32bit data
  d = ReadByte(data);
 }
}

Потому что short автоматически получит повышенный до int, этот код компилируется и работает нормально, но в нем есть ошибка, которую трудно найти - он прочитал на 2 байта меньше, чем следовало бы для c, и все последующие считанные значения будут неправильными.

Существуют ли какие-либо методы, которые можно использовать, чтобы гарантировать, что, когда c имеет тип UInt32, он не будет принимать UInt16 или другой допустимый тип?

В идеале Thing будет не может быть изменен, но если ваше решение требует этого, ничего страшного.

Ответы [ 4 ]

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

Ответ: нет для методов, которые вы предоставляете.

Разработаны, например, C# и CTS of. NET, вы не можете избежать такой ошибки кодирования.

Но, тем не менее, вы можете использовать общий c метод вроде этого:

static T Read<T>(this T instance, List<byte> data)
{
  switch ( instance )
  {
    case byte value:
      Console.WriteLine("Read byte");
      return default;
    case UInt16 value:
      Console.WriteLine("Read UInt16");
      return default;
    case UInt32 value:
      Console.WriteLine("Read Uint32");
      return default;
    default:
      string message = $"Type not supported for Read<T> (only byte or UInt16/32): "
                     + $"{typeof(T).Name}";
      throw new ArgumentException(message);
  }
}

class Thing
{
  public byte a { get; set; }
  public UInt16 b { get; set; }
  public UInt32 c { get; set; }
  public byte d { get; set; }
  public void Read(List<byte> data)
  {
    a = a.Read(data);
    b = b.Read(data);
    c = c.Read(data);
    d = d.Read(data);
  }
}

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

Тест

var list = new List<byte> { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };

var instance = new Thing();

instance.Read(list);

Вывод

Read byte
Read UInt16
Read Uint32
Read byte
3 голосов
/ 13 июля 2020

Лучший способ - написать модульные тесты, подтверждающие, что входные данные соответствуют ожидаемым выходным данным. Вы также можете выполнить рефлексию для автоматического c логирования или сделать что-то с помощью генератора кода.

Во время сборки нет ничего, что могло бы вам помочь, если только вы не хотите написать анализатор Roslyn.

1 голос
/ 13 июля 2020

Это не самая элегантная вещь, но вы можете сделать каждый из своих Read*() методов обобщенным c и выполнить проверку типа, которая выглядит излишней. К сожалению, вы не можете использовать свойство в качестве параметра out или ref, иначе это могло бы быть намного проще:

UInt16 ReadUInt16<T>(List<byte> data, T _)
{
    if (typeof(T) != typeof(UInt16))
        // Throw exception
    
    // Perform normal process and return value.
}

Тогда (это выглядит избыточно выглядящей частью) вы можете использовать это как:

c = ReadUInt16(data, c);
0 голосов
/ 14 июля 2020

Вы можете создавать типы-оболочки и использовать их в качестве возвращаемых типов для ваших методов, а также для полей поддержки ваших свойств (которые затем необходимо явно обрабатывать), например:

class WrapperTypesPreventCast
{
    private MyByte _a;
    public byte a { get { return _a; } set { _a = value; } }
    private MyUInt16 _b;
    public UInt16 b { get { return _b; } set { _b = value; } }
    private MyUInt32 _c;
    public UInt32 c { get { return _c; } set { _c = value; } }

    public void Read(List<byte> data)
    {
        _a = ReadByte(data);
        _b = ReadUInt16(data);
        //_c = ReadUInt16(data);  // Would produce a compiler error.
        _c = ReadUInt32(data);    // Compiles
    }

    // Dummy implementations
    MyByte ReadByte(List<byte> data) => 0;
    MyUInt16 ReadUInt16(List<byte> data) => 0;
    MyUInt32 ReadUInt32(List<byte> data) => 0;
}

struct MyByte
{
    public byte Value { get; }

    public MyByte(byte value)
    {
        Value = value;
    }

    public static implicit operator byte(MyByte b) => b.Value;
    public static implicit operator MyByte(byte b) => new MyByte(b);
}

struct MyUInt16
{
    public UInt16 Value { get; }

    public MyUInt16(UInt16 value)
    {
        Value = value;
    }

    public static implicit operator UInt16(MyUInt16 b) => b.Value;
    public static implicit operator MyUInt16(UInt16 b) => new MyUInt16(b);
}

struct MyUInt32
{
    public UInt32 Value { get; }

    public MyUInt32(UInt32 value)
    {
        Value = value;
    }

    public static implicit operator UInt32(MyUInt32 b) => b.Value;
    public static implicit operator MyUInt32(UInt32 b) => new MyUInt32(b);
}

Ошибки конечно все еще возможны, но из-за большей избыточности они менее вероятны. Кроме того, теперь вы можете легко создавать модульные тесты для методов Read*() и проверять, действительно ли они возвращают правильный тип (что не помогло бы вам в исходном коде, поскольку сами методы верны, просто использование - нет) .

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

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