9

我把这个结构展示给一个程序员同事,他们觉得它应该是一个可变类。他们认为没有空引用和根据需要更改对象的能力是不方便的。我真的很想知道是否有任何其他理由使它成为一个可变类。

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

7 回答 7

21

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

因此,让进程中不可变的东西在代码中不可变。使流程中可变的事物在代码中可变。

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

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

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

于 2010-03-17T23:11:51.893 回答
8

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

于 2010-03-17T20:49:35.447 回答
2

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

于 2010-03-17T20:50:18.247 回答
2

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

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

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

于 2010-03-17T21:07:03.983 回答
2

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

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;
于 2010-03-17T23:16:29.653 回答
0

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

于 2010-03-17T23:32:28.237 回答
0

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

问题是每个类对象都有效地包含两种信息:

  1. 其所有领域的内容
  2. 存在的所有引用的下落

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

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

于 2012-08-09T16:40:20.733 回答