这个不可变的结构应该是一个可变的类吗?

发布于 2024-08-25 04:12:40 字数 6636 浏览 11 评论 0原文

我向一位程序员同事展示了这个结构,他们认为它应该是一个可变的类。他们认为没有空引用和根据需要更改对象的能力会很不方便。我真的很想知道是否还有其他原因使其成为可变类。

[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
}

I showed this struct to a fellow programmer and they felt that it should be a mutable class. They felt it is inconvenient not to have null references and the ability to alter the object as required. I would really like to know if there are any other reasons to make this a mutable class.

[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
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

や三分注定 2024-09-01 04:12:42

我认为将其保留为不可变结构很好 - 但我个人只会为每个逻辑字段使用单独的变量,除非您一次在内存中拥有大量这些变量。如果您坚持使用最合适的类型(例如 ushort 表示 3-4 位数字),那么它应该不会那么昂贵 - 而且代码会很多更清晰。

I think it's fine to keep it as an immutable struct - but I would personally just use separate variables for each of the logical fields unless you're going to have huge numbers of these in memory at a time. If you stick to the most appropriate type (e.g. ushort for 3-4 digits) then it shouldn't be that expensive - and the code will be a heck of a lot clearer.

夜还是长夜 2024-09-01 04:12:42

我同意这应该是一个不可变的类型。但是为什么这个结构体应该实现 ICLoneable 和 IEquatable 接口呢?它是一种值类型。

I agree that this should be an immutable type. But why this struct should implement a ICLoneable and IEquatable interface? It is a value type.

审判长 2024-09-01 04:12:42

就我个人而言,我觉得将其保留为不可变结构是一件非常好的事情。我不建议将其更改为可变类。

根据我的经验,大多数时候,想要避免不可变结构的人都是出于懒惰而这样做的。不可变结构迫使您重新创建结构以填充参数,但良好的构造函数重载在这里可以提供巨大帮助。 (例如,看看这个 字体构造函数 - 尽管它是一个类,它实现了“克隆除此变量之外的所有内容”模式,您可以为需要更改的公共字段复制该模式。)

创建可变类会引入其他问题和开销,除非有必要,否则我会避免这些问题和开销。

Personally, I feel that leaving this as an immutable struct is a very good thing. I would not recommend changing it to a mutable class.

Most of the time, in my experience, people wanting to avoid immutable structs are doing this from laziness. Immutable structs force you to recreate the struct will full parameters, but good constructor overloads can help tremendously here. (For an example, look at this Font constructor - even though it's a class, it implements a "clone everything but this variable" pattern that you can duplicate for your common fields that need changing.)

Creating mutable classes introduces other issues and overhead that I would avoid unless necessary.

居里长安 2024-09-01 04:12:42

也许您的同事可能会对一组允许轻松“更改”各个字段的方法感到满意(导致新实例的值与第一个实例相同,但新字段除外)。

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()”更自然的语法,并允许使用以下任一方法进行等效的 null 检查: “foo == PhoneNumber.Empty”或 foo.IsEmpty。

另外......在你的 TryParse 中你不是故意的吗

return result.value != 0;

Perhaps your co-worker could be satisfied by a set of methods to allow individual fields to be easily "changed" (resulting in a new instance with the the same values as the first instance except for the new field).

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

Also, you could have a special case for an "undefined" phone number:

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

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

The "Empty" property gives a more natural syntax than either "default(PhoneNumber) or new PhoneNumber()" and allows for the equivalent of a null check with either "foo == PhoneNumber.Empty" or foo.IsEmpty.

Also... In your TryParse don't you mean to

return result.value != 0;
悲欢浪云 2024-09-01 04:12:42

可空性可以通过 PhoneNumber 轻松处理吗?

Nullability can be easily handled via PhoneNumber?

悸初 2024-09-01 04:12:42

可分段更改的数据持有者应该是结构,而不是类。虽然人们可以争论结构是否应该分段改变,但可变类会导致糟糕的数据持有者

问题是每个类对象实际上包含两种信息:

  1. 其所有字段的内容
  2. 存在的所有引用的位置

如果类对象是不可变的,那么它的引用通常并不重要。然而,当数据保存类对象是可变的时,对它的所有引用都有效地“附加”到彼此;对其中之一进行的任何突变都将有效地对所有突变进行。

如果 PhoneNumber 是一个可变结构,则可以更改 PhoneNumber 类型的一个存储位置的字段,而不会影响该类型的任何其他存储位置中的任何字段。如果有人说 var temp = Customers("Fred").MainPhoneNumber;临时扩展名=“x431”; Customers("Fred").MainPhoneNumber = temp; 这将更改 Fred 的分机而不影响其他任何人的分机。相比之下,如果 PhoneNumber 是一个可变类,则上面的代码将为 MainPhoneNumber 持有对同一对象的引用的每个人设置分机,但不会影响任何人的分机其 MainPhoneNumber 包含相同的数据,但不是同一个对象。恶心。

Data holders which are going to be piecewise-alterable should be structures, rather than classes. While one can debate whether structures should be piecewise alterable, mutable classes make lousy data holders.

The problem is that every class object effectively contains two kinds of information:

  1. The content of all of its fields
  2. The whereabouts of all the references that exist to it

If a class object is immutable, it generally won't matter what references exist to it. When a data-holding class object is mutable, however, all references to it are effectively "attached" to each other; any mutation performed on one of them will effectively be performed on all.

If PhoneNumber were a mutable struct, one could change the fields of one storage location of type PhoneNumber without affecting any fields in any other storage location of that type. If one were to say var temp = Customers("Fred").MainPhoneNumber; temp.Extension = "x431"; Customers("Fred").MainPhoneNumber = temp; that would change Fred's extension without affecting anyone else's. By contrast, if PhoneNumber were a mutable class, the above code would set the extension for everyone whose MainPhoneNumber held a reference to the same object, but not affect the extension of anyone whose MainPhoneNumber held identical data but was not the same object. Icky.

浅浅淡淡 2024-09-01 04:12:41

处理电话号码的程序是进程的模型。

因此,让进程中不可变的东西在代码中也不可变。使流程中可变的内容在代码中可变。

例如,一个进程可能包括一个人。一个人有一个名字。一个人可以更改姓名,同时保留其身份。因此,人员对象的名称应该是可变的。

一个人有一个电话号码。一个人可以更改电话号码,同时保留其身份。因此,一个人的电话号码应该是可变的。

电话号码有区号。电话号码不能更改其区号并保留其身份;您更改了区号,您现在就有了不同的电话号码。因此,电话号码的区号应该是不可变的。

A program that manipulates a phone number is a model of a process.

Therefore, make things which are immutable in the process immutable in the code. Make things which are mutable in the process mutable in the code.

For example, a process probably includes a person. A person has a name. A person can change their name while retaining their identity. Therefore, the name of a person object should be mutable.

A person has a phone number. A person can change their phone number while retaining their identity. Therefore, the phone number of a person should be mutable.

A phone number has an area code. A phone number CANNOT change its area code and retain its identity; you change the area code, you now have a different phone number. Therefore the area code of a phone number should be immutable.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文