【问题标题】:Create custom string class创建自定义字符串类
【发布时间】:2010-08-08 21:16:18
【问题描述】:

我想创建自己的 EMailAddress 类,它的作用类似于字符串类。

所以我喜欢这样做

private EMailAddress _emailAddress = "Test@Test.com";

而不是

private EMailAddress _emailAddress = new EMailAddress("Test@Test.com");

有什么方法可以完成我想要的,还是我需要使用第二种选择。由于字符串是密封的,我不能使用它,并且 = 运算符不能重载,所以我不知道如何解决这个问题......

【问题讨论】:

  • 如果它看起来像一个字符串 - 它应该是一个字符串。我不确定这是否可能,甚至是一个好主意。
  • 没有人会期望看到这种东西。只需创建自己的课程。
  • 几乎所有东西都可以看起来像一个字符串,我们应该只用字符串编程吗?电子邮件地址与字符串具有不同的属性。 “name@example.net”可以是电子邮件地址和字符串,“fish”不能是电子邮件地址。 HostName 属性或 CheckMxRecord 方法对于电子邮件地址比字符串更有意义。同时它与字符串足够接近,可以使隐式转换变得合理。不确定这是一个好主意(需要更多地了解该类及其用途),但它肯定不会自动成为一个坏主意。
  • 相反,这是一个非常好的主意。在使用 DDD 代码时,我一直需要这个。例如,强类型有助于区分“名字”和“姓氏”……在古老的帕斯卡中,有一个 type MyType = type string; 直接允许这样做。我在 C# 中想念这个。我理解这个问题,在这种情况下创建一个新类是可以避免的。
  • 对于所有认为这是个坏主意的人,请考虑使用 Enums。 Enums 价值的很大一部分是将类型信息添加到原本只是整数的内容中。

标签: c#


【解决方案1】:

您可以通过implicit 转换:

public class EMailAddress
{
    private string _address;

    public EMailAddress(string address)
    {
        _address = address;
    }

    public static implicit operator EMailAddress(string address)
    {
        // While not technically a requirement; see below why this is done.
        if (address == null)
            return null;

        return new EMailAddress(address);
    }
}

只有在转换中没有数据丢失的情况下才应该使用隐式转换。即便如此,我还是建议您谨慎使用此功能,因为它会使您的代码更难阅读。

在此示例中,当传递 null 字符串时,隐式运算符返回 null。正如 Jon Hanna 正确评论的那样,这两个 sn-ps 代码的行为不同是不可取的:

// Will assign null to the reference
EMailAddress x = null;

// Would create an EMailAddress object containing a null string
string s = null;
EMailAddress y = s;

【讨论】:

  • 这 EmailAddress x = null; 的行为与 String s = null;EmailAddress x = s; 不同,这很奇怪。最好让隐式运算符在 null 参数上返回 null。
  • 隐式转换似乎破坏了拥有一个单独类的整个想法。
  • @Anton,为什么?您是否认为我们可以隐式地将 int 转换为 double 的事实破坏了将它们分开的意义?
  • int/double 是另一种情况。拥有单独的 EMailAddress 类型有 3 个原因: 添加方法(最好通过扩展来完成);自记录代码(using EMailAddress = System.String 可能会有所帮助);并将电子邮件地址与其他字符串隔离以确保类型安全。隐式转换完全解决了这第三个也是(IMO)最重要的原因。
  • @Anton:你对类型安全提出了一个有趣的观点。就个人而言,我也可能不会为这种情况创建隐式转换。我确实有时会在等价类型之间使用显式转换,因为这样我就不用记住转换方法的名称了:)
【解决方案2】:

您可以通过将implicit operator from string 添加到您的自定义类型来做到这一点。

class EMailAddress 
{
        // ...other members

        public static implicit operator EMailAddress (string address)
        {
            return new EMailAddress(address);
        }
}

不过,我建议谨慎使用。

【讨论】:

  • 这真的很漂亮。我以为存在这样的东西。我应该尽量不要再忘记它!编辑:啊,我没听懂你的编辑。为什么您建议谨慎使用它?我想我总能读完 msdn 页面...
  • 谨慎 = 这违反了最小惊讶原则。程序员不会预料到这一点,因此会使代码变得复杂并且行为异常。但是,字符串类是有缺陷的,并且字符串作为引用类型但行为像值类型而违反了 POLA。我的许多抽象泛型类都需要无参数构造函数,因此我不能在泛型中使用 String 类。所以我创建了一个 StringMutable 类,以便它可以在我的抽象类中使用,其中:new()
【解决方案3】:

我认为这个问题是关于在 C# 应用程序中创建类型安全的更一般问题的具体案例。我这里的例子有两种类型的数据:价格和重量。它们有不同的计量单位,因此永远不要尝试为重量分配价格,反之亦然。两者都是真正的十进制值。 (我忽略了可能存在磅到公斤等转换的事实。)同样的想法可以应用于具有特定类型的字符串,例如 EmailAddress 和 UserLastName。

使用一些相当样板的代码,可以在特定类型之间进行显式转换或隐式转换:Price 和 Weight,以及底层类型 Decimal。

public class Weight
{
    private readonly Decimal _value;

    public Weight(Decimal value)
    {
        _value = value;
    }

    public static explicit operator Weight(Decimal value)
    {
        return new Weight(value);
    }

    public static explicit operator Decimal(Weight value)
    {
        return value._value;
    }
};


 public class Price {
    private readonly Decimal _value;

    public Price(Decimal value) {
        _value = value;
    }

    public static explicit operator Price(Decimal value) {
        return new Price(value);
    }

    public static explicit operator Decimal(Price value)
    {
        return value._value;
    }
};

通过“显式”运算符覆盖,人们可以使用这些类获得一组更具限制性的事情。每次从一种类型更改为另一种类型时,您都必须手动设置大小写。例如:

    public void NeedsPrice(Price aPrice)
    {
    }

    public void NeedsWeight(Weight aWeight)
    {
    }

    public void NeedsDecimal(Decimal aDecimal)
    {
    }
  public void ExplicitTest()
    {

        Price aPrice = (Price)1.23m;
        Decimal aDecimal = 3.4m;
        Weight aWeight = (Weight)132.0m;

        // ok
        aPrice = (Price)aDecimal;
        aDecimal = (Decimal)aPrice;

        // Errors need explicit case
        aPrice = aDecimal;
        aDecimal = aPrice;

        //ok
        aWeight = (Weight)aDecimal;
        aDecimal = (Decimal) aWeight;

        // Errors need explicit cast
        aWeight = aDecimal;
        aDecimal = aWeight;

        // Errors (no such conversion exists)
        aPrice = (Price)aWeight;
        aWeight = (Weight)aPrice;

        // Ok, but why would you ever do this.
        aPrice = (Price)(Decimal)aWeight;
        aWeight = (Weight)(Decimal)aPrice;

        NeedsPrice(aPrice);   //ok
        NeedsDecimal(aPrice); //error
        NeedsWeight(aPrice);  //error

        NeedsPrice(aDecimal);   //error
        NeedsDecimal(aDecimal); //ok
        NeedsWeight(aDecimal);  //error

        NeedsPrice(aWeight);   //error
        NeedsDecimal(aWeight); //error
        NeedsWeight(aWeight);  //ok
    }

只需通过将代码中的“显式”替换为“隐式”将“显式”运算符更改为“隐式”运算符,就可以在没有任何额外工作的情况下来回转换到底层的 Decimal 类。这使得价格和重量的行为更像小数,但您仍然不能将价格更改为重量。这通常是我正在寻找的类型安全级别。

 public void ImplicitTest()
    {
        Price aPrice = 1.23m;
        Decimal aDecimal = 3.4m;
        Weight aWeight = 132.0m;

        // ok implicit cast
        aPrice = aDecimal;
        aDecimal = aPrice;

        // ok implicit cast
        aWeight = aDecimal;
        aDecimal = aWeight;

        // Errors 
        aPrice = aWeight;
        aWeight = aPrice;


        NeedsPrice(aPrice);   //ok
        NeedsDecimal(aPrice); //ok
        NeedsWeight(aPrice);  //error

        NeedsPrice(aDecimal);   //ok
        NeedsDecimal(aDecimal); //ok
        NeedsWeight(aDecimal);  //ok

        NeedsPrice(aWeight);   //error
        NeedsDecimal(aWeight); //ok
        NeedsWeight(aWeight);  //ok
    }    

当对 String 而不是 Decimal 执行此操作时。我喜欢 Thorarin 关于检查 null 并在转换中传回 null 的答案的想法。例如

public static implicit operator EMailAddress(string address)
{
    // Make
    //      EmailAddress myvar=null 
    // and
    //      string aNullString = null;
    //      EmailAddress myvar = aNullString;
    // give the same result.
    if (address == null)
        return null;

    return new EMailAddress(address);
}

要让这些类作为 Dictionary 集合的键,您还需要实现 Equals、GetHashCode、operator == 和 operator !=

为了使这一切更容易,我创建了一个可以扩展的 ValueType 类,ValueType 类调用除转换运算符之外的所有内容的基类型。

【讨论】:

  • 这个问题的最完整答案,完美,谢谢!您是否有机会将您提到的 ValueType 基类添加到您的答案中?
【解决方案4】:
  1. 隐式运算符应将一个 null 转换为另一个 null,除非转换为的类型不可为空,在这种情况下,它应该在 null 上出错。
  2. 如果您正在编写包含 Uri 的内容,请不要强迫使用它的拥有 Uri 的人自己完成获取字符串的工作。电子邮件地址自然适合 mailto: uris,因此虽然这不是一个很好的例子,但它很接近。

例子:

public class EmailAddress
{
    private string _value;
    private static bool IsValidAddress(string address)
    {
        //whether to match RFC822 production or have something simpler,
        //but excluding valid but unrealistic addresses, is an impl. choice
        //out of scope.
        return true;
    }
    public EMailAddress(string value)
    {
        if(value == null)
            throw new ArgumentNullException();
        if(!IsValidAddress(value))
            throw new ArgumentException();
        _value = value;
    }
    public EmailAddress(Uri uri)
    {
        if(value == null)
            throw new ArgumentNullException();
        if(!uri.Scheme != "mailto")
            throw new ArgumentException();
        string extracted = uri.UserInfo + "@" + uri.Host;
        if(!IsValidAddress(extracted))
           throw new ArgumentException();
        _value = extracted;
    }
    public override string ToString()
    {
        return _value;
    }
    public static implicit operator EMailAddress(string value)
    {
        return value == null ? null : new EMailAddress(value);
    }
    public static implicit operator EMailAddress(Uri uri)
    {
        return value == null ? null : new EMailAddress(uri);
    }
}

【讨论】:

  • 在类中添加电子邮件地址验证是有意义的。但是,可能失败的隐式转换有点代码味道,imo。
  • 另外,我有点失望你没有实现完整的 RFC822(或取代它的 RFC5322)验证;)
  • 完全验证只是意味着在示例中添加一个非常长的正则表达式,而且通常不是真正想要的。至于是否是不好的代码气味,代码气味是相对的。它会让我反对允许一个隐含的(如果我自己创建这个类,可能会让我反对到我不会这样做的程度),但我不会认为这是一个明确的禁忌。
【解决方案5】:

正如您所说,您不能从字符串继承,但可以使用扩展方法对其进行扩展。

因此,您可以创建一些特定于电子邮件的扩展方法来处理您想要放入 EmailAddress 类的任何内容。

【讨论】:

  • 如何使用扩展方法处理带有字符串的赋值?
  • 未分配。我认为除了作业之外还有其他理由开设课程。
  • 就个人而言,我更喜欢创建自己的 EMailAddress 类,而不是通过扩展方法将特定于电子邮件地址的垃圾添加到 System.String。但是有一个相当不错的MailAddress 类:)
【解决方案6】:

作为@Thorarin 代码的后续:

您可以将其用作基本代码并添加普通字符串类所具有的额外功能:

public class VarChar
{
        private string _content;

        public VarChar(string argContent)
        {
            _content = argContent;
        }

        public static implicit operator VarChar(string argContent)
        {
            if (argContent == null)
                return null;

            return new VarChar(argContent);
        }

        public static implicit operator string(VarChar x) => x._content;

        public override string ToString()
        {
            return _content;
        }
}

所以下面的代码可以正常工作:

VarChar tempVarChar = "test";

string tempTest = tempVarChar; // = "test"

var tempText = tempVarChar.ToString();

tempVarChar = tempVarChar + "_oke";

tempText = tempVarChar; // = "test_oke"

【讨论】:

  • 您从未解释过null 部分?
  • 查看 Thorarin 的帖子
  • 你说“请看下面为什么这样做”,但从未解释过原因。
  • 好的,我删除了那个文本。正如我明确指出的,这段代码是基于 Thorarin 的帖子
猜你喜欢
  • 1970-01-01
  • 2014-12-03
  • 2014-02-13
  • 2021-12-05
  • 1970-01-01
  • 2019-06-02
  • 1970-01-01
  • 1970-01-01
  • 2018-09-07
相关资源
最近更新 更多