Эта неизменная структура должна быть изменяемым классом?

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

[Serializable]
public struct PhoneNumber : IEquatable<PhoneNumber>
{
    private const int AreaCodeShift = 54;
    private const int CentralOfficeCodeShift = 44;
    private const int SubscriberNumberShift = 30;
    private const int CentralOfficeCodeMask = 0x000003FF;
    private const int SubscriberNumberMask = 0x00003FFF;
    private const int ExtensionMask = 0x3FFFFFFF;


    private readonly ulong value;


    public int AreaCode
    {
        get { return UnmaskAreaCode(value); }
    }

    public int CentralOfficeCode
    {
        get { return UnmaskCentralOfficeCode(value); }
    }

    public int SubscriberNumber
    {
        get { return UnmaskSubscriberNumber(value); }
    }

    public int Extension
    {
        get { return UnmaskExtension(value); }
    }


    public PhoneNumber(ulong value)
        : this(UnmaskAreaCode(value), UnmaskCentralOfficeCode(value), UnmaskSubscriberNumber(value), UnmaskExtension(value), true)
    {

    }

    public PhoneNumber(int areaCode, int centralOfficeCode, int subscriberNumber)
        : this(areaCode, centralOfficeCode, subscriberNumber, 0, true)
    {

    }

    public PhoneNumber(int areaCode, int centralOfficeCode, int subscriberNumber, int extension)
        : this(areaCode, centralOfficeCode, subscriberNumber, extension, true)
    {

    }

    private PhoneNumber(int areaCode, int centralOfficeCode, int subscriberNumber, int extension, bool throwException)
    {
        value = 0;

        if (areaCode < 200 || areaCode > 989)
        {
            if (!throwException) return;
            throw new ArgumentOutOfRangeException("areaCode", areaCode, @"The area code portion must fall between 200 and 989.");
        }
        else if (centralOfficeCode < 200 || centralOfficeCode > 999)
        {
            if (!throwException) return;
            throw new ArgumentOutOfRangeException("centralOfficeCode", centralOfficeCode, @"The central office code portion must fall between 200 and 999.");
        }
        else if (subscriberNumber < 0 || subscriberNumber > 9999)
        {
            if (!throwException) return;
            throw new ArgumentOutOfRangeException("subscriberNumber", subscriberNumber, @"The subscriber number portion must fall between 0 and 9999.");
        }
        else if (extension < 0 || extension > 1073741824)
        {
            if (!throwException) return;
            throw new ArgumentOutOfRangeException("extension", extension, @"The extension portion must fall between 0 and 1073741824.");
        }
        else if (areaCode.ToString()[1] == '9')
        {
            if (!throwException) return;
            throw new ArgumentOutOfRangeException("areaCode", areaCode, @"The second digit of the area code cannot be greater than 8.");
        }
        else
        {
            value |= ((ulong)(uint)areaCode << AreaCodeShift);
            value |= ((ulong)(uint)centralOfficeCode << CentralOfficeCodeShift);
            value |= ((ulong)(uint)subscriberNumber << SubscriberNumberShift);
            value |= ((ulong)(uint)extension);
        }
    }


    public override bool Equals(object obj)
    {
        return obj != null && obj.GetType() == typeof(PhoneNumber) && Equals((PhoneNumber)obj);
    }

    public bool Equals(PhoneNumber other)
    {
        return this.value == other.value;
    }

    public override int GetHashCode()
    {
        return value.GetHashCode();
    }

    public override string ToString()
    {
        return ToString(PhoneNumberFormat.Separated);
    }

    public string ToString(PhoneNumberFormat format)
    {
        switch (format)
        {
            case PhoneNumberFormat.Plain:
                return string.Format(@"{0:D3}{1:D3}{2:D4}{3:#}", AreaCode, CentralOfficeCode, SubscriberNumber, Extension).Trim();
            case PhoneNumberFormat.Separated:
                return string.Format(@"{0:D3}-{1:D3}-{2:D4} {3:#}", AreaCode, CentralOfficeCode, SubscriberNumber, Extension).Trim();
            default:
                throw new ArgumentOutOfRangeException("format");
        }
    }

    public ulong ToUInt64()
    {
        return value;
    }


    public static PhoneNumber Parse(string value)
    {
        var result = default(PhoneNumber);
        if (!TryParse(value, out result))
        {
            throw new FormatException(string.Format(@"The string ""{0}"" could not be parsed as a phone number.", value));
        }
        return result;
    }

    public static bool TryParse(string value, out PhoneNumber result)
    {
        result = default(PhoneNumber);

        if (string.IsNullOrEmpty(value))
        {
            return false;
        }

        var index = 0;
        var numericPieces = new char[value.Length];

        foreach (var c in value)
        {
            if (char.IsNumber(c))
            {
                numericPieces[index++] = c;
            }
        }

        if (index < 9)
        {
            return false;
        }

        var numericString = new string(numericPieces);
        var areaCode = int.Parse(numericString.Substring(0, 3));
        var centralOfficeCode = int.Parse(numericString.Substring(3, 3));
        var subscriberNumber = int.Parse(numericString.Substring(6, 4));
        var extension = 0;

        if (numericString.Length > 10)
        {
            extension = int.Parse(numericString.Substring(10));
        }

        result = new PhoneNumber(
            areaCode,
            centralOfficeCode,
            subscriberNumber,
            extension,
            false
        );

        return result.value != 0;
    }

    public static bool operator ==(PhoneNumber left, PhoneNumber right)
    {
        return left.Equals(right);
    }

    public static bool operator !=(PhoneNumber left, PhoneNumber right)
    {
        return !left.Equals(right);
    }

    private static int UnmaskAreaCode(ulong value)
    {
        return (int)(value >> AreaCodeShift);
    }

    private static int UnmaskCentralOfficeCode(ulong value)
    {
        return (int)((value >> CentralOfficeCodeShift) & CentralOfficeCodeMask);
    }

    private static int UnmaskSubscriberNumber(ulong value)
    {
        return (int)((value >> SubscriberNumberShift) & SubscriberNumberMask);
    }

    private static int UnmaskExtension(ulong value)
    {
        return (int)(value & ExtensionMask);
    }
}

public enum PhoneNumberFormat
{
    Plain,
    Separated
}
9
задан ChaosPandion 18 March 2010 в 04:37
поделиться

6 ответов

Программа, которая манипулирует телефонным номером, является моделью процесса.

Поэтому делайте неизменяемые в процессе вещи неизменными в коде. Сделайте изменяемые в процессе вещи изменяемыми в коде.

Например, процесс, вероятно, включает человека. У человека есть имя. Человек может изменить свое имя, сохранив при этом свою личность. Следовательно, имя объекта человека должно быть изменяемым.

У человека есть номер телефона. Человек может изменить свой номер телефона, сохранив при этом свою личность. Следовательно, номер телефона человека должен быть изменяемым.

У телефонного номера есть код города. Телефонный номер НЕ МОЖЕТ изменить свой код города и сохранить свою идентичность; вы меняете код города, теперь у вас есть другой номер телефона. Поэтому код города в телефонном номере должен быть неизменным.

21
ответ дан 4 December 2019 в 06:40
поделиться

Я согласен с тем, что это должен быть неизменяемый тип. Но почему эта структура должна реализовывать интерфейс ICLoneable и IEquatable? Это тип значения.

2
ответ дан 4 December 2019 в 06:40
поделиться

Лично я считаю, что оставить this как неизменяемую структуру - это очень хорошо. Я бы не рекомендовал менять его на изменяемый класс.

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

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

2
ответ дан 4 December 2019 в 06:40
поделиться

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

public PhoneNumber ApplyAreaCode(int areaCode)
{
  return new PhoneNumber(
    areaCode, 
    centralOfficeCode, 
    subscriberNumber, 
    extension);
}

Кроме того, вы могли бы иметь специальный случай для "неопределенного" номера телефона:

public static PhoneNumber Empty
{ get {return default(PhoneNumber); } }

public bool IsEmpty
{ get { return this.Equals(Empty); } }

Свойство "Empty" дает более естественный синтаксис, чем "default(PhoneNumber) или new PhoneNumber()", и позволяет эквивалент проверки на нуль с помощью "foo == PhoneNumber.Empty" или foo.IsEmpty.

Также... В вашем TryParse вы не имеете в виду

return result.value != 0;
2
ответ дан 4 December 2019 в 06:40
поделиться

Я думаю, что вполне можно сохранить его как неизменяемую структуру - но лично я бы просто использовал отдельные переменные для каждого из логических полей, если вы не собираетесь иметь огромное их количество в памяти одновременно. Если вы придерживаетесь наиболее подходящего типа (например, ushort для 3-4 цифр), то это не должно быть настолько дорого - и код будет намного понятнее.

8
ответ дан 4 December 2019 в 06:40
поделиться

Возможность обнуления можно легко обработать с помощью PhoneNumber?

0
ответ дан 4 December 2019 в 06:40
поделиться
Другие вопросы по тегам:

Похожие вопросы: