如何避免API设计中的“参数过多”问题?


160

我有这个API函数:

public ResultEnum DoSomeAction(string a, string b, DateTime c, OtherEnum d, 
     string e, string f, out Guid code)

我不喜欢 因为参数顺序变得不必要地重要。添加新字段变得越来越困难。很难看到正在传递的内容。很难将方法重构为较小的部分,因为这会带来在子函数中传递所有参数的另一项开销。代码更难读。

我想到了一个最明显的想法:让一个对象封装数据并将其传递,而不是一个个地传递每个参数。这是我想出的:

public class DoSomeActionParameters
{
    public string A;
    public string B;
    public DateTime C;
    public OtherEnum D;
    public string E;
    public string F;        
}

这使我的API声明减少为:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

真好 看起来很无辜,但实际上我们进行了巨大的更改:我们引入了可变性。因为我们之前所做的实际上是在堆栈上传递一个匿名的不可变对象:函数参数。现在我们创建了一个非常可变的新类。我们创建了一种操作调用者状态的功能。糟透了。现在我希望我的对象是不变的,我该怎么办?

public class DoSomeActionParameters
{
    public string A { get; private set; }
    public string B { get; private set; }
    public DateTime C { get; private set; }
    public OtherEnum D { get; private set; }
    public string E { get; private set; }
    public string F { get; private set; }        

    public DoSomeActionParameters(string a, string b, DateTime c, OtherEnum d, 
     string e, string f)
    {
        this.A = a;
        this.B = b;
        // ... tears erased the text here
    }
}

如您所见,我实际上重新创建了最初的问题:参数太多。显然,这不是要走的路。我该怎么办?实现这种不变性的最后一个选择是使用如下所示的“只读”结构:

public struct DoSomeActionParameters
{
    public readonly string A;
    public readonly string B;
    public readonly DateTime C;
    public readonly OtherEnum D;
    public readonly string E;
    public readonly string F;        
}

这使我们可以避免构造函数使用太多参数并实现不变性。实际上,它解决了所有问题(参数订购等)。然而:

那时候我感到困惑,决定写这个问题:C#中最简单的方法是在不引入可变性的情况下避免“参数过多”的问题?可以使用只读结构来实现此目的,但又不会有不好的API设计吗?

说明:

  • 请假设没有违反单一责任原则。在我的原始情况下,该函数只是将给定的参数写入单个DB记录。
  • 我没有在寻找给定功能的特定解决方案。我正在寻求针对此类问题的通用方法。我对解决“参数过多”问题特别感兴趣,而又不会引入可变性或糟糕的设计。

更新

此处提供的答案具有不同的优点/缺点。因此,我想将其转换为社区Wiki。我认为每个带有代码示例和优点/缺点的答案都将为将来解决类似问题提供很好的指南。我现在正试图找出方法。


干净代码:Robert C. Martin和Martin Fowler的Refactoring书中的敏捷软件技巧手册,涵盖了这一点
Ian Ringrose

1
如果您使用具有可选参数的 C#4,那么Builder-solution是否不是多余的?
khellang 2011年

1
我可能很愚蠢,但是考虑到DoSomeActionParameters是一个一次性对象,在方法调用后将其丢弃,因此我看不到这是一个问题。
Vilx- 2011年

6
请注意,我并不是说您应该避免在结构中使用只读字段。不变结构是一种最佳实践,只读字段可帮助您创建自我文档化的不变结构。我的观点是,您不应依赖于观察到的永不更改的永不更改的字段,因为这不能保证只读字段可以为您提供结构。这是更一般性建议的一种具体情况,即您不应将值类型视为引用类型;他们是非常不同的动物。
埃里克·利珀特

7
@ssg:我也很喜欢。我们已经在C#中添加了促进不可变性的功能(如LINQ),同时又添加了促进可变性的功能(如对象初始化程序)。最好有一个更好的语法来促进不可变类型。我们正在认真考虑,并提出了一些有趣的想法,但我不希望在下一个版本中出现任何此类问题。
Eric Lippert

Answers:


22

框架中包含的一种样式通常类似于将相关参数分组到相关类中(但又存在可变性问题):

var request = new HttpWebRequest(a, b);
var service = new RestService(request, c, d, e);
var client = new RestClient(service, f, g);
var resource = client.RequestRestResource(); // O params after 3 objects

仅当所有字符串都相关时,才将所有字符串压缩为一个字符串数组。从参数的顺序来看,它们似乎并不完全相关。
icktoofay

同意,此外,只有在参数具有很多相同类型的情况下,这才起作用。
Timo Willemsen

这与我在问题中的第一个示例有何不同?
Sedat Kapanoglu 2011年

好吧,即使在.NET Framework中,这也是通常使用的样式,这表明您的第一个示例在做的事情上是非常正确的,即使它引入了一些小的可变性问题(尽管该示例显然来自另一个库)。顺便问好问题。
Teoman Soygul

2
随着时间的流逝,我越来越趋向于这种风格。显然,数量众多的“参数过多”问题可以通过良好的逻辑组和抽象来解决。最后,它使代码更具可读性和模块化。
Sedat Kapanoglu 2013年

87

结合使用构建器和特定于域的语言样式的API--Fluent接口。该API较为冗长,但具有智能感知功能,可以快速输入并易于理解。

public class Param
{
        public string A { get; private set; }
        public string B { get; private set; }
        public string C { get; private set; }


  public class Builder
  {
        private string a;
        private string b;
        private string c;

        public Builder WithA(string value)
        {
              a = value;
              return this;
        }

        public Builder WithB(string value)
        {
              b = value;
              return this;
        }

        public Builder WithC(string value)
        {
              c = value;
              return this;
        }

        public Param Build()
        {
              return new Param { A = a, B = b, C = c };
        }
  }


  DoSomeAction(new Param.Builder()
        .WithA("a")
        .WithB("b")
        .WithC("c")
        .Build());

+1-这种方法(我通常将其称为“流畅接口”)也正是我所想到的。
Daniel Pryden 2011年

+1-这就是我在相同情况下的结果。除了只有一个类,并且DoSomeAction是它的一种方法。
Vilx- 2011年

+1我也想到了这一点,但是由于我是流利的界面的新手,所以我不知道它是否非常适合。感谢您验证我自己的直觉。
马特

在问这个问题之前,我还没有听说过Builder模式,因此这对我来说是一次有见地的体验。我想知道这有多普遍吗?因为任何没有听说过该模式的开发人员都可能在没有文档的情况下难以理解用法。
Sedat Kapanoglu 2011年

1
@ssg,在这些情况下很常见。例如,BCL具有连接字符串构建器类。
塞缪尔·内夫

10

您所拥有的信息可以肯定地表明所讨论的类违反了“ 单一职责原则”,因为它具有过多的依赖项。寻找将这些依赖项重构为Facade依赖项群集的方法。


1
我仍然会通过引入一个或多个参数对象来进行重构,但是很显然,如果将所有参数都移动到单个不可变类型,那么您将一事无成。诀窍是寻找更紧密相关的参数簇,然后将这些簇中的每一个重构为单独的参数对象。
Mark Seemann

2
您可以将每个组本身变成一个不变的对象。每个对象只需要带有几个参数,因此,在实际的参数数量保持不变的情况下,任何单个构造函数中使用的参数数量都会减少。
Mark Seemann

2
+1 @ssg:每当我设法说服自己这样的事情时,随着时间的推移,我通过将有用的抽象从需要这种级别参数的大型方法中剔除出来,证明自己是错误的。埃文斯(Evans)撰写的DDD书可能会给您一些有关如何思考这一问题的想法(尽管您的系统听起来与应用此类模式可能距离很远)-(而且无论如何这都是一本好书)。
Ruben Bartelink 2011年

3
@Ruben:没有理智的书会说“在一个好的OO设计中,一个类不应具有超过5个属性”。类是按逻辑分组的,这种上下文关系不能基于数量。但是,我们开始违反良好的OO设计原则之前, C#的不变性支持开始在一定数量的属性上引发问题。
Sedat Kapanoglu 2011年

3
@Ruben:我没有判断你的知识,态度或脾气。我希望你也这样做。我并不是说您的建议不好。我的意思是,即使在最完美的设计中,我的问题也会出现,这似乎是一个被忽略的问题。尽管我理解为什么有经验的人会问一些关于最常见错误的基本问题,但经过两轮澄清后,变得越来越难了。我必须再次说,完美的设计很可能会遇到这个问题。并感谢您的支持!
Sedat Kapanoglu 2011年

10

只需将参数数据结构从a更改class为a struct,就可以了。

public struct DoSomeActionParameters 
{
   public string A;
   public string B;
   public DateTime C;
   public OtherEnum D;
   public string E;
   public string F;
}

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code) 

现在,该方法将获得其自己的结构副本。方法无法观察到对参数变量所做的更改,调用者也无法观察到对变量进行方法更改。实现隔离而不会一成不变。

优点:

  • 最容易实施
  • 基本机制中行为的最小变化

缺点:

  • 不变性并不明显,需要开发人员注意。
  • 不必要的复制以保持不变性
  • 占用堆栈空间

+1是正确的,但不能解决“直接暴露田野是邪恶的”这一部分。我不确定如果选择使用字段而不是该API的属性,情况会变得更糟。
塞达

@ssg-因此使它们成为公共属性,而不是字段。如果将其视为永远不会包含代码的结构,则无论使用属性还是字段都不会有太大的不同。如果您决定给它提供代码(例如验证之类的东西),那么您肯定要为其赋予属性。至少对于公共场所,没有人会对这种结构上存在的不变性有任何幻想。就像参数一样,必须通过该方法进行验证。
Jeffrey L Whitledge 2011年

哦,是的。太棒了!谢谢。
Sedat Kapanoglu 2011年

8
我认为,“暴露场就是邪恶”规则适用于面向对象设计中的对象。我提出的结构只是一个裸机参数容器。由于您的直觉是与某种不变的事物一起使用的,所以我认为这样的基本容器可能适合这种情况。
Jeffrey L Whitledge 2011年

1
@JeffreyLWhitledge:我真的不喜欢裸露的场结构是邪恶的想法。令我震惊的是,这等同于说起子是邪恶的,人们应该使用锤子,因为起子的尖端会使钉头凹陷。如果需要打钉子,则应使用锤子,但是如果需要打钉子,则应使用螺丝刀。在许多情况下,外场结构恰好是完成这项工作的正确工具。附带说一句,具有get / set属性的结构真正是正确的工具的地方要少得多(在大多数情况下,...
超级猫

6

如何在数据类中创建构建器类。数据类会将所有设置器设为私有,只有构建器才能设置它们。

public class DoSomeActionParameters
    {
        public string A { get; private set; }
        public string B  { get; private set; }
        public DateTime C { get; private set; }
        public OtherEnum D  { get; private set; }
        public string E  { get; private set; }
        public string F  { get; private set; }

        public class Builder
        {
            DoSomeActionParameters obj = new DoSomeActionParameters();

            public string A
            {
                set { obj.A = value; }
            }
            public string B
            {
                set { obj.B = value; }
            }
            public DateTime C
            {
                set { obj.C = value; }
            }
            public OtherEnum D
            {
                set { obj.D = value; }
            }
            public string E
            {
                set { obj.E = value; }
            }
            public string F
            {
                set { obj.F = value; }
            }

            public DoSomeActionParameters Build()
            {
                return obj;
            }
        }
    }

    public class Example
    {

        private void DoSth()
        {
            var data = new DoSomeActionParameters.Builder()
            {
                A = "",
                B = "",
                C = DateTime.Now,
                D = testc,
                E = "",
                F = ""
            }.Build();
        }
    }

1
+1这是一个非常有效的解决方案,但我认为维护这样一个简单的设计决定太麻烦了。特别是当“只读结构”解决方案“非常”接近理想时。
Sedat Kapanoglu 2011年

2
如何解决“参数过多”的问题?语法可能不同,但是问题看起来相同。这不是批评,我只是好奇,因为我不熟悉这种模式。
alexD 2011年

1
@alexD解决了具有过多函数参数并使对象保持不变的问题。只有构建器类可以设置私有属性,并且一旦获得参数对象就无法更改它。问题在于它需要大量的脚手架代码。
marto 2011年

5
解决方案中的参数对象不是一成不变的。保存构建器的人甚至可以在构建后编辑参数
astef 2014年

1
就@astef而言,由于当前已编写了一个DoSomeActionParameters.Builder实例,因此可以用来创建和配置一个DoSomeActionParameters实例。在Build()Builder的属性进行后续更改后,将继续修改原始 DoSomeActionParameters实例的属性,对的后续调用Build()将继续返回相同的DoSomeActionParameters实例。应该是这样public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }
培根

6

我不是C#程序员,但我相信C#支持命名参数:(F#可以,并且C#在很大程度上适合于此类事情)它可以: http //msdn.microsoft.com/zh-cn/library/dd264739 .aspx#Y342

因此,调用您的原始代码将变为:

public ResultEnum DoSomeAction( 
 e:"bar", 
 a: "foo", 
 c: today(), 
 b:"sad", 
 d: Red,
 f:"penguins")

这样就不需要花费更多的空间/思想来创建对象并拥有所有的好处,因为您根本没有改变过这个系统中正在发生的一切。您甚至不必重新编码即可表明参数已命名

编辑:这是我发现的一篇文章。 http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ 我应该提到C#4.0支持命名参数,而3.0没有


确实是一种改进。但这仅解决了代码的可读性,并且仅当开发人员选择使用命名参数时才这样做。很容易忘记指定名称并放弃收益。编写代码本身无济于事。例如,将功能重构为较小的功能,然后将数据传递到单个包含的数据包中。
Sedat Kapanoglu 2011年

6

为什么不仅仅制作一个强制不变性的接口(即仅使用吸气剂)?

从本质上讲,这是您的第一个解决方案,但是您可以强制该函数使用接口来访问参数。

public interface IDoSomeActionParameters
{
    string A { get; }
    string B { get; }
    DateTime C { get; }
    OtherEnum D { get; }
    string E { get; }
    string F { get; }              
}

public class DoSomeActionParameters: IDoSomeActionParameters
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public OtherEnum D { get; set; }
    public string E { get; set; }
    public string F { get; set; }        
}

并且函数声明变为:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

优点:

  • 没有像这样的堆栈空间问题 struct解决方案
  • 使用语言语义的自然解决方案
  • 不变性显而易见
  • 灵活(消费者可以根据需要使用其他类别)

缺点:

  • 一些重复的工作(两个不同实体中的相同声明)
  • 开发人员必须猜测这DoSomeActionParameters是可以映射到的类IDoSomeActionParameters

+1我不知道为什么我没想到吗?:)我想我认为对象仍然会受参数过多的构造函数的影响,但事实并非如此。是的,这也是一个非常有效的解决方案。我能想到的唯一问题是,对于API用户而言,找到支持给定接口的正确类名并不是一件容易的事。需要最少文档的解决方案更好。
Sedat Kapanoglu 2011年

我喜欢这一个,复制和地图的知识与ReSharper的处理,并用具体类的默认构造函数,我可以提供缺省值
安东尼约翰斯顿

2
我不喜欢这种方法。引用了的任意实现IDoSomeActionParameters并想要在其中捕获值的人无法知道持有引用是否足够,或者是否必须将值复制到其他对象。可读的接口在某些情况下很有用,但不是使事物变得不可变的手段。
supercat 2012年

可以将“ IDoSomeActionParameters参数”强制转换为DoSomeActionParameters并进行更改。开发人员甚至可能没有意识到他们正在绕过使参数不可更改的尝试
Joseph Simpson

3

我知道这是一个老问题,但是我认为我不得不接受我的建议,因为我不得不解决相同的问题。现在,不可否认,我的问题与您的问题稍有不同,因为我还有其他要求,即不希望用户自己构造该对象(数据的所有混合都来自数据库,因此我可以在内部阻止所有构造)。这使我可以使用私有构造函数和以下模式;

    public class ExampleClass
    {
        //create properties like this...
        private readonly int _exampleProperty;
        public int ExampleProperty { get { return _exampleProperty; } }

        //Private constructor, prohibiting construction outside of this class
        private ExampleClass(ExampleClassParams parameters)
        {                
            _exampleProperty = parameters.ExampleProperty;
            //and so on... 
        }

        //The object returned from here will be immutable
        public ExampleClass GetFromDatabase(DBConnection conn, int id)
        {
            //do database stuff here (ommitted from example)
            ExampleClassParams parameters = new ExampleClassParams()
            {
                ExampleProperty = 1,
                ExampleProperty2 = 2
            };

            //Danger here as parameters object is mutable

            return new ExampleClass(parameters);    

            //Danger is now over ;)
        }

        //Private struct representing the parameters, nested within class that uses it.
        //This is mutable, but the fact that it is private means that all potential 
        //"damage" is limited to this class only.
        private struct ExampleClassParams
        {
            public int ExampleProperty { get; set; }
            public int AnotherExampleProperty { get; set; }
            public int ExampleProperty2 { get; set; }
            public int AnotherExampleProperty2 { get; set; }
            public int ExampleProperty3 { get; set; }
            public int AnotherExampleProperty3 { get; set; }
            public int ExampleProperty4 { get; set; }
            public int AnotherExampleProperty4 { get; set; } 
        }
    }

2

您可以使用Builder风格的方法,尽管根据您DoSomeAction方法的复杂性,这可能是一个重量级的任务。遵循以下原则:

public class DoSomeActionParametersBuilder
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public OtherEnum D { get; set; }
    public string E { get; set; }
    public string F { get; set; }

    public DoSomeActionParameters Build()
    {
        return new DoSomeActionParameters(A, B, C, D, E, F);
    }
}

public class DoSomeActionParameters
{
    public string A { get; private set; }
    public string B { get; private set; }
    public DateTime C { get; private set; }
    public OtherEnum D { get; private set; }
    public string E { get; private set; }
    public string F { get; private set; }

    public DoSomeActionParameters(string a, string b, DateTime c, OtherEnum d, string e, string f)
    {
        A = a;
        // etc.
    }
}

// usage
var actionParams = new DoSomeActionParametersBuilder
{
    A = "value for A",
    C = DateTime.Now,
    F = "I don't care for B, D and E"
}.Build();

result = foo.DoSomeAction(actionParams, out code);

啊,马托击败了我,向了建造者的建议!
克里斯·怀特

2

除了manji响应之外,您可能还希望将一个操作拆分为几个较小的操作。比较:

 BOOL WINAPI CreateProcess(
   __in_opt     LPCTSTR lpApplicationName,
   __inout_opt  LPTSTR lpCommandLine,
   __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
   __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
   __in         BOOL bInheritHandles,
   __in         DWORD dwCreationFlags,
   __in_opt     LPVOID lpEnvironment,
   __in_opt     LPCTSTR lpCurrentDirectory,
   __in         LPSTARTUPINFO lpStartupInfo,
   __out        LPPROCESS_INFORMATION lpProcessInformation
 );

 pid_t fork()
 int execvpe(const char *file, char *const argv[], char *const envp[])
 ...

对于那些不了解POSIX的人来说,创建孩子就像这样简单:

pid_t child = fork();
if (child == 0) {
    execl("/bin/echo", "Hello world from child", NULL);
} else if (child != 0) {
    handle_error();
}

每个设计选择都代表着可能进行的操作之间的权衡。

PS。是的-与构建器类似-仅相反(即在被叫方而不是呼叫方)。在这种特定情况下,它可能比构造器更好,也可能更好。


这是一个很小的操作,但是对于任何违反单一责任原则的人来说,这都是一个很好的建议。我的问题仅在完成所有其他设计改进后才发生。
Sedat Kapanoglu 2011年

UPS。抱歉-在您进行编辑之前,我已经准备好一个问题,并在以后发布了-因此,我没有注意到它。
Maciej Piechotka 2011年

2

这与Mikeys略有不同,但是我想做的是使整个事情写得尽可能少

public class DoSomeActionParameters
{
    readonly string _a;
    readonly int _b;

    public string A { get { return _a; } }

    public int B{ get { return _b; } }

    DoSomeActionParameters(Initializer data)
    {
        _a = data.A;
        _b = data.B;
    }

    public class Initializer
    {
        public Initializer()
        {
            A = "(unknown)";
            B = 88;
        }

        public string A { get; set; }
        public int B { get; set; }

        public DoSomeActionParameters Create()
        {
            return new DoSomeActionParameters(this);
        }
    }
}

DoSomeActionParameters可能是不变的,并且不能直接创建,因为其默认构造函数是private

初始化程序不是一成不变的,而只是一个传输

用法利用了初始化器上的初始化器(如果您不满意的话),并且我可以在初始化器默认构造函数中使用默认值

DoSomeAction(new DoSomeActionParameters.Initializer
            {
                A = "Hello",
                B = 42
            }
            .Create());

这些参数在这里是可选的,如果您需要一些参数,可以将它们放在Initializer的默认构造函数中

验证可以在Create方法中进行

public class Initializer
{
    public Initializer(int b)
    {
        A = "(unknown)";
        B = b;
    }

    public string A { get; set; }
    public int B { get; private set; }

    public DoSomeActionParameters Create()
    {
        if (B < 50) throw new ArgumentOutOfRangeException("B");

        return new DoSomeActionParameters(this);
    }
}

所以现在看起来

DoSomeAction(new DoSomeActionParameters.Initializer
            (b: 42)
            {
                A = "Hello"
            }
            .Create());

我知道还是有点怪异,但还是要尝试一下

编辑:将create方法移动到parameter对象中的静态对象,并添加一个传递初始化程序的委托,以消除调用中的某些怪异现象

public class DoSomeActionParameters
{
    readonly string _a;
    readonly int _b;

    public string A { get { return _a; } }
    public int B{ get { return _b; } }

    DoSomeActionParameters(Initializer data)
    {
        _a = data.A;
        _b = data.B;
    }

    public class Initializer
    {
        public Initializer()
        {
            A = "(unknown)";
            B = 88;
        }

        public string A { get; set; }
        public int B { get; set; }
    }

    public static DoSomeActionParameters Create(Action<Initializer> assign)
    {
        var i = new Initializer();
        assign(i)

        return new DoSomeActionParameters(i);
    }
}

所以通话现在看起来像这样

DoSomeAction(
        DoSomeActionParameters.Create(
            i => {
                i.A = "Hello";
            })
        );

1

使用该结构,但具有公共属性,而不是公共字段:

•每个人(包括FXCop和Jon Skeet)都同意公开公共场所是有害的。

Jon和FXCop会很满意,因为您公开的是属性而不是字段。

•埃里克·利珀特(Eric Lippert)等人说,依靠只读字段实现不变性是一个谎言。

埃里克(Eric)将很满意,因为使用属性可以确保该值仅设置一次。

    private bool propC_set=false;
    private date pC;
    public date C {
        get{
            return pC;
        }
        set{
            if (!propC_set) {
               pC = value;
            }
            propC_set = true;
        }
    }

一个半不变的对象(可以设置但不能更改值)。适用于值和引用类型。


+1也许可以将私有只读字段和仅公共获取方法属性组合到一个结构中,以允许较不冗长的解决方案。
Sedat Kapanoglu 2011年

变异结构上的公共读写属性this比公共字段邪恶得多。我不确定您为什么认为只设置一次值就可以使事情“正常”;如果以结构的默认实例开头,则与this-mutating属性相关的问题仍然存在。
supercat 2012年

0

当我遇到相同问题时,我在项目中使用了萨缪尔(Samuel)答案的一种变体:

class MagicPerformer
{
    public int Param1 { get; set; }
    public string Param2 { get; set; }
    public DateTime Param3 { get; set; }

    public MagicPerformer SetParam1(int value) { this.Param1 = value; return this; }
    public MagicPerformer SetParam2(string value) { this.Param2 = value; return this; }
    public MagicPerformer SetParam4(DateTime value) { this.Param3 = value; return this; }

    public void DoMagic() // Uses all the parameters and does the magic
    {
    }
}

并使用:

new MagicPerformer().SeParam1(10).SetParam2("Yo!").DoMagic();

在我的情况下,参数是有意修改的,因为setter方法不允许所有可能的组合,而只公开了它们的常见组合。那是因为我的一些参数相当复杂,并且为所有可能的情况编写方法将是困难且不必要的(很少使用疯狂的组合)。


无论如何,这都是一个可变的类。
Sedat Kapanoglu 2011年

@ssg-是的,是的。该DoMagic在其合同,虽然它不会修改对象。但是我想它不能防止意外修改。
Vilx- 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.