C#中具有固定长度的字符串对象

16

我有一个类,在其中我想使用长度固定的字符串。这么做的原因是该类会“序列化”为一个文本文件,并且每个值都需要具有固定的长度。我希望避免为每个值编写守卫子句,而是让类来处理。

因此,我大约有30个属性,看起来像这样

    public String CompanyNumber
    {
        get 
        {
            return m_CompanyNumber.PadLeft(5, ' ');
        }
        set 
        {
            if (value.Length > 5) 
            {
                throw new StringToLongException("The CompanyNumber may only have 5 characters", "CompanyNumber");
            }
            m_CompanyNumber = value; 
        }
    } 

我希望有一个字符串可以自行处理这个问题。目前我有以下内容:

public class FixedString
{
    String m_FixedString;

    public FixedString(String value)
    {
        if (value.Length > 5) 
        {
            throw new StringToLongException("The FixedString value may consist of 5 characters", "value");
        }
        m_FixedString= value;
    }

    public static implicit operator FixedString(String value)
    {
        FixedString fsv = new FixedString(value);
        return fsv;
    }

    public override string ToString()
    {
         return m_FixedString.PadLeft(5,' ');
    }
}

我对这个解决方案的问题在于,我不能在“编译时”设置字符串的长度。

如果最终它能看起来像这样就太理想了。

public FixedString<5> CompanyNumber { get; set; }

6
不要去那里,只需抛出异常然后结束这一天的工作。 - CodesInChaos
我理解这个问题,但我不认为你试图做的是一个好的解决方案。相反,我建议尝试使用代码编织器来解决你遇到的重复代码问题。例如,对于.NET,Fody是一个很好的免费开源工具。当然,你需要编写一个插件来让它按照你的意愿工作。 - Andrew Savinykh
2
顺便问一下,那不应该是 StringTooLongException 而不是 StringToLongException 吗?StringToLong 听起来像是转换。 - Bob
或者只需使用new StringBuilder() { MaxCapacity = 5 } - Slai
5个回答

16

我会更进一步地质疑设计。这个解决方案将内部应用程序状态和存储格式这两个问题混合在一起,它们本应该分开。

你可以使用MaxLengthAttribute修饰每个字符串属性,然后对其进行验证,但你从你的存储格式进行(反)序列化的代码应该完全分离。它可以使用相同的属性来获取存储字段的长度(如果存在这种幸运的巧合),但你的内部表示不应该"知道"存储细节。


让我想到......我不太确定它是否适用,因为它比仅仅是存储等更加复杂。但这个观点似乎是正确的+1。 - Bongo
对于DTO(数据传输对象),除了“我有一个具有这样和那样属性的类”之外的几乎所有问题都是独立的问题,应该使用服务类架构来提供解决方案,最好是通过依赖注入的方式。 - Marc L.
我发现你的回答非常有趣,作为鼓励用于另一个问题 http://stackoverflow.com/questions/37206837/is-there-a-data-conversion-object-principle-pattern 我希望在那里见到你 :) - Bongo

8

FixedString的大小作为构造函数参数,而不是值本身。

public class FixedString
{
   private string value;
   private int length;
   public FixedString(int length)
   {
      this.length = length;
   }

   public string Value 
   {
       get{ return value; }
       set
       {
            if (value.Length > length) 
            {
                 throw new StringToLongException("The field may only have " + length + " characters");
            }
           this.value = value; 
       }
   }
}

使用您的类进行初始化,仅在更改时设置Value

public class MyClass
{
    private FixedString companyNumber = new FixedString(5);

    public string CompanyNumber
    {
        get{ return companyNumber.Value; }
        set{ companyNumber.Value = value; }
    }
}

1
但是那样的话,OP就不能使用“隐式转换运算符”了,对吧? - Tim Schmelter
@TimSchmelter 可以在消费者中这样做。 string companyNumber = myClassInstance.CompanyNumber(显然需要类似的隐式操作符实现,但不包含在我的答案中,而是在 OP 中)。 - Jamiec
但这样你就无法在编译时进行安全检查了。CompanyNumber 将被转换为一个没有长度验证的 FixedString - Tim Schmelter
我会看看这是否适用于我,但我想知道是否可以将其缩小到属性而不是fullprop。 - Bongo
@Jamiec 表示,使用隐式操作符和安全性是不可能的。 - Bongo

5
您可以像这样定义一个接口:
public interface ILength
{
    int Value { get; }
}

实现该接口的某个结构体:

public struct LengthOf5 : ILength
{
    public int Value { get { return 5; } }
}

public struct LengthOf10 : ILength
{
    public int Value { get { return 10; } }
}

接着:

public class FixedString<T> where T : struct, ILength
{
    String m_FixedString;

    public FixedString(String value)
    {
        if (value.Length > default(T).Value)
        {
            throw new ArgumentException("The FixedString value may consist of " + default(T).Value + " characters", "value");
        }
        m_FixedString = value;
    }

    public static implicit operator FixedString<T>(String value)
    {
        FixedString<T> fsv = new FixedString<T>(value);
        return fsv;
    }

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

说实话,我不知道我是否喜欢这种解决方案,但这是我能想到的最好的方法来解决你的问题。

我必须编写一个结构体,以实现我想要的大小,从我的角度来看,这不是最好的解决方案,但至少比我的好;) - Bongo
是的,对于这个我不喜欢。但至少这样你可以保持从string的隐式操作符。 - Alessandro D'Andria

2
您可以在字符串属性上放置一个属性,然后在某个时间(例如按钮单击或类似操作)验证所有属性。请保留HTML标签。
using System.ComponentModel.DataAnnotations;

public class MyObject
{
   [StringLength(5)]
   public String CompanyName { get; set; }
}

public void Save(MyObject myObject)
{
   List<ValidationResult> results = new List<ValidationResult>();
   ValidationContext context = new ValidationContext(myObject, null, null);
   bool isValid = Validator.TryValidateObject(myObject, context, results);

   if (!isValid)
   {
      foreach (ValidationResult result in results)
      {
         // Do something
      }
   }
}

更多关于DataAnnotations的信息请点击此处


1
"StringLength" 属性指定字符串的最大长度,但 OP 希望该字符串恰好为 5 个字符长,并自动提供额外的填充。 - Kapol
好的,他可以将两种解决方案结合起来:使用固定类进行填充,并使用数据注释进行验证... - Alessandro
@Kapol,StringLengthAttribute确实有一个属性可以指定最小长度。如果属性是[StringLength(5, MinimumLength = 5)],那么这个答案仍然是可行的。 - Will Ray

1
我认为您最初创建固定长度字符串的想法非常有效,严格模拟系统域并使用类型系统进行验证是一种非常吸引人的想法。像这样的问题似乎在F#社区中经常出现。
不幸的是,在.NET上下文中,无法使用您建议的类型定义(FixedString<5>)。
到目前为止,一些答案已经谈到了解决方法、替代方案或其他想法,我想回答的是为什么您不能在C#中实现最初请求的内容。
首先,让我们看看如何在任意语言中实现这个功能:

模板:你可以使用模板系统在C++中做类似于这样的事情。正如Eric Lippert在他关于泛型和模板之间差异的文章中所说,“你可以将模板看作是一个花哨的搜索和替换机制”(https://blogs.msdn.microsoft.com/ericlippert/2009/07/30/whats-the-difference-part-one-generics-are-not-templates/)。

.NET泛型在很多方面相比之下要简单得多。泛型类型允许您对类型进行参数化,但不允许对值进行参数化,并且开放类型在运行时解析,而模板是完全编译时结构。

依赖类型:一些语言支持一种称为依赖类型(https://en.wikipedia.org/wiki/Dependent_type)的特性。这使您能够定义依赖于值的类型。许多支持此功能的语言都是针对定理证明而非通用开发而设计的。

Idris可能是一个正在积极开发的通用编程语言(尽管不太知名),支持这个特性(请参见http://www.idris-lang.org/)。

C#

C#不支持这两个特性,因此,不幸的是,您无法以可以被编译器严格验证的方式解决这个问题。

我认为这里提供了很多好的建议,可以告诉您如何在C#中实现这样的功能,但它们都归结为运行时验证。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接