有更高效的方法定义类似的公共属性吗?

28

我有一个类,里面有近20个公共属性。这些属性都是字符串类型,并且它们填充了来自数据库不同表的数据。

除此之外,设置方法非常普通,而获取方法很特殊,因为我需要调用特定的方法。目前每个属性都需要这样做(见下文)。

我的问题是:是否有另一种更有效的方法来实现这一点,即不必手动定义每个公共属性?

class myclass
{
     private string _Firstname;
     private string _Lastname;
     .....
     public string Firstname
     {
         get {
             return ModifyStringMethod(this._Firstname);
         }

         set {
             this._Firstname = value;
         }
     }
}

如上所述,每个公共属性看起来都是相同的。get调用ModifyStringMethod并将私有成员作为参数传递,而set只是设置私有成员。


它必须是getter吗?还是可以使用反射做些什么?也许有一个方法GetPropertyValue(string propertyName) - ste-fu
不确定我理解你的意思。但在这个确切的例子中,我需要返回修改后的字符串(在我的情况下,字符串修改方法只是截断字符串并在末尾添加“...”,如果一个字符串太长而无法显示)。因此,如果您的意思是这样,get需要返回修改后的值。 - Thomas
我已经发布了一个例子。 - ste-fu
4
顺便提一下,对于非只读属性,在getter中进行此类操作需要谨慎。这可能会导致出现反直觉的行为,例如像“x.Firstname = x.Firstname”这样的代码行,大多数人不会预期它会更改Firstname的值。您可以考虑创建另一个只读属性来呈现“转换后”的视图,例如“DisplayFirstname”。 - Dan Bryant
我非常赞同Dan的观点。如果这是为了显示而做的,我可能会进一步创建一个独立的门面类来覆盖此操作,因此MyClass具有读/写属性,而MyClassFacade具有只读属性,用于公开修改后的MyClass属性以供显示。 - Dan J
显示剩余2条评论
11个回答

18
您可以尝试使用T4模板进行自动代码生成。当您有简单、重复的代码模式,并且不希望某些情况与其他情况略有不同时,它们非常完美。
只需定义一个包含属性名称列表的XML文件,然后使用T4模板生成每个属性的部分类即可。

2
s/hate/have/,希望如愿 ;) - O. R. Mapper
2
我本来要发布T4,但你比我先到了。相对于代码片段,T4绝对更好,因为你只需要添加更改,然后修改并重新生成T4,所有属性都具有相同的逻辑。 - Wai Ha Lee
我同意 T4 的观点。为了完整起见,你能否发布一个示例实现? - Jeroen Vannevel

12

另一个选项与Dion V.的解决方案类似,但它利用隐式转换使属性在外部表现得像普通字符串,并使使用简单自动属性成为可能。但这仅在ModifyStringMethod是静态的并且不需要来自类外部的参数时才有效。

public struct EllipsisString
{
    private string _value;

    public string Value {get { return _value; }}

    public EllipsisString(string value)
    {
        _value = value;
    }

    // implicit conversion from string so it is possible to just assign string to the property
    public static implicit operator EllipsisString(string value)
    {
        return new EllipsisString(value);
    }

    public static implicit operator string(EllipsisString original)
    {
        return SpecialMethod(original.Value);
    }

    public override string ToString()
    {
        return SpecialMethod(Value);
    }

    private static string SpecialMethod(string value)
    {
        return value + "...";
    }
}

使用方法很简单:

    private EllipsisString FirstName { get; set; }

    public void Method()
    {
        FirstName = "Thomas";
        Console.WriteLine(FirstName);
        Console.WriteLine(FirstName.Value);
    }

3
如果您的属性具有特殊的行为和特征,超出了简单字符串所提供的范围,那么它就不应该是一个简单的字符串。将这种行为添加到底层类型中(在Euphorics示例中为EllipsisString),并添加隐式转换以使它们在类外更易处理(如果需要)。与Mark Seemans关于领域建模的文章 loosley 相关。http://blog.ploeh.dk/2015/01/19/from-primitive-obsession-to-domain-modelling - Casey

6

PostSharp是另一种选择。

你只需在类上应用一个属性,并使用“get; set;”语法编写属性。

PostSharp是一个.NET工具,使开发人员能够将代码方面应用于它们的程序集、命名空间、类或方法。

具体来说,PostSharp使开发人员通过将属性应用于代码块来编写更少的代码,稍后将具有该方面的代码反映进入并与所选代码块一起执行。这种方法显着减少了在代码库中冗余的“管道”。

常见用例包括以下内容:

  • 日志记录

  • 安全性

  • 撤消/重做

  • INotifyPropertyChanged

  • 异常处理


1
虽然我喜欢这个答案,但它基本上是基于链接的。如果链接失效,你的答案的实用性就会受到影响。我建议添加一个代码片段来演示你的解决方案,并为那些不熟悉PostSharp的人提供一些简要介绍。谢谢! - Paul Sasik
1
即使没有断链的论点,代码示例也会改善这个答案。一旦我获得了PostSharp,这个答案仍然没有告诉我该怎么做(即使PostSharp有一个清晰的教程,这个答案也不是自包含的)。 - Kyeotic

4
如果您可以将属性设置为虚拟的,您就可以使用Castle Dynamic Proxy的拦截器。拦截器包含在调用给定方法时执行的行为。在这种情况下,我们将ModifyStringMethod应用于字符串属性的返回值。
如何实现: 1)添加对NuGet包Castle.Core的引用。 2)定义您的拦截器。
public class ModifyStringMethodInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        invocation.Proceed();
        if (invocation.Method.Name.StartsWith("get_") && 
            invocation.Method.ReturnType == typeof(string))
        {
            invocation.ReturnValue = 
              ModifyStringMethod((string)invocation.ReturnValue);
        }
    }

    private static string ModifyStringMethod(string input)
    {
        return (input ?? "") + "MODIFIED";
    }
}

上面的示例有一个Intercept方法,当您的属性被调用时会调用该方法。在示例中,您可以看到invocation.Proceed(),这将继续调用属性。
然后它检查是否为get_属性并返回一个字符串。
 if (invocation.Method.Name.StartsWith("get_") &&
     invocation.Method.ReturnType == typeof(string))

然后修改该方法的返回值。
 invocation.ReturnValue = ModifyStringMethod((string)invocation.ReturnValue);

3)使用虚方法定义您希望添加此行为的对象(请注意,我也可以在此处使用自动实现属性)奖励

public class Intercepted
{
    public virtual string A { get; set; }
}

4) 接下来使用DynamicProxy中的ProxyGenerator类创建对象实例,

例如:

 public class Program
{
    static void Main(string[] args)
    {
        var pg = new ProxyGenerator();

        //  Intercepted will be an instance of Intercepted class with the           
        //  ModifyStringMethodInterceptor applied to it 
        var intercepted = pg.CreateClassProxy<Intercepted>(new ModifyStringMethodInterceptor());

        intercepted.A = "Set ... ";
        Console.WriteLine(intercepted.A);
        Console.ReadLine();

    }
}

输出结果为:

Set ... MODIFIED

这里的好处是您的对象是“干净”的,例如它们不需要知道ModifyStringMethodInterceptor并且可以包含自动实现的属性,如果您有大量这些对象,则将大大减少代码量。
更进一步,如果您需要进一步控制,可以通过向类添加属性来应用此行为。
[AttributeUsage(AttributeTargets.Method)]
public class ModifyStringMethodAttribute : Attribute
{
}

然后定义对象为:

 public class Intercepted
 {
     public virtual string A { [ModifyStringMethod] get; set; }
 }

并且拦截器的修改:

if (invocation.Method.ReturnType == typeof(string) && 
    invocation.Method.GetCustomAttributes(true)
    .OfType<ModifyStringMethodAttribute>().Any())
        {
            invocation.ReturnValue = 
                  ModifyStringMethod((string)invocation.ReturnValue);
        }

检查属性,然后应用方法调用。


4
过去,当我面临这种情况时,我使用VS中的自定义代码片段轻松创建属性。请参见此链接:HERE
然后,当您需要添加新属性时,只需调用代码片段并必要时填写项目名称即可。
虽然这不能完全消除拥有许多相似属性的需要,但它确实使它们更容易创建(与上述使用T4模板的方法类似)。

3
然而,我个人不太喜欢这种解决方案,你可以尝试以下做法:
class MyClass
{
    private IDictionary<string, string> propertyValueByName = new Dictionary<string, string>(); 

    public string this[string propertyName]
    {
        get { return propertyValueByName[propertyName]; }
        set { propertyValueByName[propertyName] = ModifyStringMethod(value); }
    }

    public string FirstName
    {
        get { return this["FirstName"]; }
        set { this["FirstName"] = value; }
    }

    public string LastName
    {
        get { return this["LastName"]; }
        set { this["LastName"] = value; }
    }
}

3
一个使用反射的例子。
class MyClass
{
    public string FirstName { private get; set; }

    public string LastName { private get; set; }


    public string GetModifiedValue(string propertyName)
    {
        var prop = this.GetType().GetProperty(propertyName);
        return ModifyStringMethod((string)prop.GetValue(this, null));
    }
}

所以,为了获取每个修改后的值,而不是使用 MyClass.FirstName,你应该使用 MyClass.GetModifiedValue("FirstName")


3
你可以在这里创建自定义代码片段,这是我为自己创建的一个示例,用于自动化创建带有更改通知的属性,你可以将其用作模板:
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>propnot</Title>
            <Shortcut>propnot</Shortcut>
            <Description>Code snippet for property and backing field with property change event</Description>
            <Author>Radin Gospodinov</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>type</ID>
                    <ToolTip>Property type</ToolTip>
                    <Default>int</Default>
                </Literal>
                <Literal>
                    <ID>property</ID>
                    <ToolTip>Property name</ToolTip>
                    <Default>MyProperty</Default>
                </Literal>
                <Literal>
                    <ID>field</ID>
                    <ToolTip>The variable backing this property</ToolTip>
                    <Default>myVar</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp">
      <![CDATA[private $type$ $field$;

    public $type$ $property$
    {
        get { return this.$field$;}
        set {
    if(this.$field$ != value)   {
        $field$ = value;
        this.RaisePropertyChanged(() => this.$property$);
      }
      }
    }
    $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

3
如果您真的想采用这种方法,那么使用类似于T4模板或CodeSmith的代码生成可能是正确的选择,但是我同意@DanBryant的观点,以此方式构建属性会导致一个反直觉的类。我期望以下代码可以正常工作:
X.FirstName = "Some random long name";
Assert.AreEqual("Some random long name", X.FirstName);

根据您的评论,使用您的类设计可能无法正常工作(取决于在ModifyStringMethod中截断长度,实际上您可能会得到X.FireName ==“Some rand ...”。这似乎是错误的。

更好的方法可能是在属性行为之外实现修改,可能是通过扩展方法。所以像这样:

public static class FormatStringExtensions {
    public static string ModifyStringForOutput(this string me) {
        if (me.Length > 10) {
            return me.Substring(0, 10) + "...";
        }
        return me;
    }
};

这将使您能够使用自动属性定义数据类:

public class myclass {
    public string FirstName { get; set; }
    public string LastName {get; set; }
};

然后,使用扩展方法根据需要从属性中修改字符串值:

    var instance = new myclass();

    instance.FirstName = "01234567890123";

    Console.WriteLine("Original Name {0}\nModified Name {1}\n", 
                      instance.FirstName,
                      instance.FirstName.ModifyStringForOutput());

这样可以让您的属性像正常属性一样正常工作,同时为您提供了一个简单的方式来访问格式化的字符串(如果需要)。

2
你可以定义一个自定义类,继承自DynamicObject
public class MyExpando : DynamicObject
{
    Dictionary<string, object> dictionary = new Dictionary<string, object>();

    //Want to create properties on initialization? Do it in the constructor
    public MyExpando()
    {
        dictionary.Add("PreferredName", "Darth Sidious");
        dictionary.Add("GreatDialog", "Something, something, darkside!");
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        bool success = dictionary.TryGetValue(binder.Name, out result);
        if (success)
            result = ModifiedValue(result); 
        return success;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        dictionary[binder.Name] = value;
        return true;
    }
    private string ModifiedValue(object val)
    {
        //Modify your string here.

        if (val.ToString() != "Darth Sidious")
            return "Something something complete";
        return val.ToString();
    }
}

想在构造函数之外创建一个属性吗?那么你可以这样做。
dynamic x = new MyExpando();
x.FirstName = "Sheev";
x.LastName = "Palpatine"
Console.WriteLine(x.PreferredName + " says : \"" + x.GreatDialog + "\"");
Console.ReadKey();

这个东西的好处是你可以实现 INotifyPropertyChanged 接口,然后在 TrySetMember 方法中调用它,也可以通过在 TryGetMember 或 TrySetMember 方法中根据属性名抛出异常来限制对 "Properties" setter 或 getter 的访问,并且可以更改继承自 MyExpando 的类的字典的访问修饰符以模拟属性继承。
以下是如何限制属性 setter 访问的示例。
    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        if (!dictionary.ContainsKey(binder.Name))
            return false;
        dictionary[binder.Name] = value;
        return true;
    }

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