【问题标题】:Why are 'out' parameters in .NET a bad idea?为什么 .NET 中的“输出”参数是个坏主意?
【发布时间】:2008-09-25 15:38:37
【问题描述】:

为什么 .NET 中的“out”参数是个坏主意?

我最近被问到这个问题,但我没有真正的答案,除了它只是不必要地使应用程序复杂化。还有什么其他原因?

【问题讨论】:

    标签: .net


    【解决方案1】:

    嗯,我认为他们不是一个坏主意。 Dictionary<K, V> 有一个 TryGetValue 方法,这是一个很好的例子,为什么输出参数有时是一件非常好的事情。

    当然,您不应过度使用此功能,但根据定义,这不是一个坏主意。尤其是在 C# 中,您必须在函数声明 and 调用中写下 out 关键字,这使得发生的事情一目了然。

    【讨论】:

    • 问题是您不必为对象编写 out 关键字,并且它们仍然可以在导致副作用的方法中更改。我个人更喜欢函数式编程范式。
    • 不,你错了。对象的内容可能会改变,但对象本身不能交换到另一个对象,除非您使用refout 传递它。这就是这些修饰符的真正意义所在!是的,我同意这严重影响了功能范式。但有时它仍然有一个优点。
    • 微软还说你应该“避免输出参数”docs.microsoft.com/en-us/previous-versions/dotnet/…
    【解决方案2】:

    如果您关心通过接受不变性和消除副作用来编写可靠的代码,那么输出参数绝对是一个糟糕的主意。它迫使您创建可变变量来处理它们。 (并不是说 C# 无论如何都支持只读的方法级变量(至少在我使用的版本中,3.5)。

    其次,它们通过强制开发人员设置和处理接收输出值的变量来减少函数的组合性。这是令人讨厌的仪式。您不能随便使用它们轻松地编写表达式。因此,调用这些函数的代码很快就会变成一大堆命令式的混乱,从而提供了很多隐藏错误的地方。

    【讨论】:

    • out 参数在一些特殊情况下很有用,正是因为它们阻止了函数组合:例如,当一个函数可能返回一个 null 值,而你想要强制调用者在函数结果传递给下一个函数之前显式检查null
    • 这就是 Option/Maybe monad 的用途。
    • 重复使用这个包含 128 个参数的该死方法也很痛苦。有一位同事喜欢这样做,我很难维护他的代码。
    • 如果您有一个可能返回值或不返回值的方法,则可空变量是一个更好的选择。如果要返回多个变量,请考虑使用元组。
    【解决方案3】:

    我会说你给出的答案是最好的。如果可能,您应该始终远离out 参数进行设计,因为这通常会使代码变得不必要地复杂。

    但这几乎适用于任何模式。如果不需要,请不要使用它。

    【讨论】:

    • 我认为断言“它通常会使代码变得不必要地复杂”的笼统声明是一个非常粗略的概括。在很多情况下,使用 out 参数会简化您的设计,而不是使其复杂化。这就是为什么它首先出现在语言中。
    • @Ben Collins:我知道,这就是为什么我说“通常”、“如果可能”等。
    【解决方案4】:

    Out - 我认为参数是个坏主意。它们增加了副作用的风险并且难以调试。唯一好的解决方案是函数结果,问题是:对于函数结果,您必须为每个复杂结果创建一个元组或一个新类型。现在它在 c# 中应该相当容易,使用匿名类型和泛型。

    顺便说一句:我也讨厌副作用。

    【讨论】:

      【解决方案5】:

      问题的措辞是of the "Have you stopped hitting your wife?" variety——它假定out 参数一定是个坏主意。在某些情况下 out 参数可能会被滥用,但是……

      • 它们实际上非常适合 C# 语言。它们类似于 ref 参数,只是方法保证为它赋值并且调用者不需要初始化变量。

      • 函数的正常返回值被推送到调用和退出函数的堆栈中。当你提供一个 out 参数时,你给方法一个特定的内存地址来保存结果。这也允许多个返回值,尽管使用结构通常更有意义。

      • 它们非常适合 TryParse 模式,还为其他事物提供元数据,例如 SQL-CLR 的情况,其中 out 参数映射到 out 参数存储过程签名。

      【讨论】:

      • LOL .. 在 Hanselman 列出“为什么在 .NET 中输出参数是个坏主意?是吗?”之后,我发现了这一点。作为一个面试问题,我的第一个想法是,“你什么时候不再打你的妻子了?”你的回答表明我们在想同样的事情。
      【解决方案6】:

      嗯,这个没有明确的答案,但是 out 参数的简单用例是Int.TryParse("1",out myInt)

      XXX.TryParse 方法的工作是,它将某种类型的值转换为另一种类型,它会返回一个布尔标志来指示转换成功或失败,其中一部分是转换后的值,另一部分是转换后的值,由该方法中的 out 参数携带。

      此 TryParse 方法是在 .NET 2.0 中引入的,以克服以下事实:如果转换失败,XXX.Parse 将引发异常,并且您必须在语句周围添加 try/catch 才能捕获它。

      所以基本上这取决于你的方法在做什么,以及方法调用者期望什么,如果你正在做一个返回某种形式的响应代码的方法,那么out参数可以用来执行返回的方法结果。

      无论如何,微软在他们的design guideline (MSDN page)

      中说“避免使用 out 参数

      【讨论】:

        【解决方案7】:

        我不认为他们是。滥用它们是一个坏主意,但这适用于任何编码实践/技术。

        【讨论】:

          【解决方案8】:

          我必须同意 GateKiller 的观点。如果微软将它们用作基础库的一部分,我无法想象参数会太糟糕。但是,与所有事物一样,最好适度。

          【讨论】:

            【解决方案9】:

            out”很棒!

            clear 声明参数包含方法返回的值。

            编译器还会强制你初始化它(如果我们使用它作为返回参数,它应该被初始化)。

            【讨论】:

            • 是的,'out' 是一种改进,但它并没有改变这些天'out' 参数被滥用到无止境。
            【解决方案10】:

            使用 out 参数并不总是一个坏主意。通常对于尝试基于某种形式的输入创建对象的代码,最好提供一个没有参数和布尔返回值的 Try 方法,而不是强制方法使用者包装 try catch到处都是块,更不用说更好的性能了。 示例:

            bool TryGetSomeValue(out SomeValue value, [...]);
            

            在这种情况下,输出参数是个好主意。

            另一种情况是您希望避免在方法之间传递代价高昂的大型结构。 例如:

            void CreateNullProjectionMatrix(out Matrix matrix);
            

            这个版本避免了昂贵的结构复制。

            但是,除非出于特定原因需要 out,否则应该避免。

            【讨论】:

            • 您的第二个示例可以重写为 matrix.CreateNullProjectionMatrix (),即成员函数。或者使用构造函数参数来指定初始矩阵状态。或者使用派生类型根据类型进行初始化(即 ProjectionMatrix : Matrix)。
            • 第二个例子的重点是它避免了结构复制。如果将矩阵作为函数返回值返回,矩阵将在函数退出之前被推入堆栈,在函数返回后弹出,在我的示例中这不会发生
            • 性能不能作为参数。重构代码以便在返回时不复制结构是编译器应该(并且可以,通常是微不足道的)做的事情。将此任务推送给程序员是错误的,out 根本没有任何论据。
            • Konrad,查看 XNA 类库,出于性能原因,out 被广泛使用。 msdn.microsoft.com/en-us/library/bb195649.aspx 例如。欢迎浏览更多内容。
            【解决方案11】:

            它们只是稍微破坏了方法/函数的语义。一个方法通常应该接受一堆东西,然后吐出一个东西。至少,这就是大多数程序员的想法(我认为)。

            【讨论】:

              【解决方案12】:

              他们是个好主意。因为有时您只想从一个方法返回多个变量,而您不想为此创建一个“沉重”的类结构。包装类结构可以隐藏信息流动的方式。

              我用这个来:

              • 一次调用即可返回密钥和 IV 数据
              • 将人名拆分为不同的实体(名字、中间名、姓氏)
              • 拆分地址

              【讨论】:

              • 如果你不想要一个“沉重”的类结构,那么在 C# 中使用结构而不是成熟的类有什么问题?
              • 因为它会增加代码的复杂性。您向额外类型添加了依赖项,而不是简单的调用。
              • “因为它会增加代码的复杂性”。使用 out 参数而不是用户定义的类型可以降低代码编写的复杂性。但它使代码阅读起来更加复杂。 Out 参数混淆了方法参数传递的正常语义。大多数人发现参数是输入,返回值是输出的函数更容易理解。如果您想完全使用 out 参数,那么完全避免返回值会更直接,而是要求所有输出作为 out 参数。然后使用您的代码的人可以在一个地方找到输出。
              • 错了。至少在 2018 年。返回一个元组。
              • @Dave 如果您返回的东西的复杂性需要,您仍然可以返回一个带有命名参数的结构 - 但是,在大多数情况下,GvS 提到的用例需要返回多个参数可以使用元组和解构来处理值:docs.microsoft.com/en-us/dotnet/csharp/deconstruct -- 通过解构,您的值始终都被命名。
              【解决方案13】:

              out 变量无论如何都不错,如果我们需要从一个函数返回多个(特别是 2 个)变量,它确实是一个很酷的东西。有时,仅仅为了返回 2 个变量而创建自定义对象真的很乏味,out 是最终的解决方案。

              【讨论】:

              • c# 元组更适合这个
              【解决方案14】:

              我想说你的答案是正确的;它通常是不必要的复杂性,通常有一个更简单的设计。您在 .NET 中使用的大多数方法都不会改变它们的输入参数,因此使用这种语法的一次性方法有点令人困惑。代码变得不那么直观了,而且在记录不完整的方法的情况下,我们无法知道该方法对输入参数做了什么。

              此外,没有参数的方法签名将触发代码分析/FxCop 违规,如果这是您关心的指标。在大多数情况下,有一种更好的方法来实现使用“out”参数的方法的意图,并且可以重构该方法以返回有趣的信息。

              【讨论】:

                【解决方案15】:

                我会说我避免它的原因是因为让一个方法处理数据并返回一个逻辑数据是最简单的。如果它需要返回更多数据,它可能是进一步抽象或形式化返回值的候选者。

                一个例外是如果返回的数据有点三级,比如可以为空的布尔值。它可以是真/假或未定义。 out 参数可以帮助解决其他逻辑类型的类似想法。

                我有时使用的另一种方法是当您需要通过不允许使用“更好”方法的接口获取信息时。例如使用 .Net 数据访问库和 ORM。当您需要返回更新的对象图(在 UPDATE 之后)或新的 RDBMS 生成的标识符 (INSERT) 以及受语句影响的行数时。 SQL 在一条语句中返回所有这些,因此对方法的多次调用将不起作用,因为您的数据层、库或 ORM 仅调用一个通用 INSERT/UPDATE 命令,并且无法存储值以供以后检索。

                从不使用动态参数列表或输出参数等理想做法并不总是可行的。同样,如果输出参数是真正正确的方法,请三思而后行。如果是这样,那就去吧。 (但一定要好好记录!)

                【讨论】:

                  【解决方案16】:

                  这就像 Tanqueray 人喜欢说的那样——一切都要适度。

                  我肯定会强调不要过度使用,因为它会导致“在 c# 中编写 c”,就像在 Python 中编写 java 一样(你不会接受使新语言特别的模式和习语,而是简单地用旧语言思考并转换为新语法)。不过,与往常一样,如果它有助于您的代码更优雅、更有意义,那就摇摆不定。

                  【讨论】:

                    【解决方案17】:

                    我认为在某些情况下它们是一个好主意,因为它们允许从一个函数返回多个对象,例如:

                    DateTime NewDate = new DateTime();
                    if (DateTime.TryParse(UserInput, out NewDate) == false) {
                        NewDate = DateTime.Now;
                    }
                    

                    非常有用:)

                    【讨论】:

                    • 初始化变量是无关紧要的,因为 out 关键字总是被视为方法的未分配参数(直到设置)。我也会避免使用等于错误的语法,而只是否定表达式本身。
                    • 我永远无法理解这种“if (b == false)”背后的基本原理。您不应该将其扩展为更明确的“if ((b == false) == true)”吗? :)
                    • @Constantin,我发现更恶心的是看到这样的东西: if (x == true) return true;否则返回假;
                    • Python 做同样的事情更理智,IMO,它的多参数:var1,var2 = functionWith2ReturnValues()。在这种范式下,参数进来,输出出来。故事结局。如果没有口译员的帮助,我不知道这有多容易,但几乎可以肯定它更干净、更合乎逻辑。
                    【解决方案18】:

                    我没有看到提到的一点是 out 关键字还要求调用者指定 out。这一点很重要,因为它有助于确保调用者明白调用这个函数会修改他的变量。

                    这就是我从不喜欢在 C++ 中使用引用作为输出参数的原因之一。调用者可以在不知道自己的参数会被修改的情况下轻松调用方法。

                    【讨论】:

                      【解决方案19】:

                      out 关键字是必需的(请参阅SortedDictionart.TryGetValue)。它意味着通过引用传递,并告诉编译器以下内容:

                      int value;
                      some_function (out value);
                      

                      没问题,some_function 没有使用通常是错误的未初始化值。

                      【讨论】:

                      • 如果你不使用 out 值,你永远不必担心这一点,因为它们都在一行上: int value = some_function();比输出值更干净、更简单、更容易。
                      • @weberc2:我的意思是您可能希望一个函数返回两个或多个值,例如:SortedDictionary.TryGetValue (),并且“out”参数由函数,并且在稍后在函数中使用时不应被视为未初始化的值。我的答案中的代码只是一个例子。你是对的,value = some_function() 在这种特殊情况下会更有意义,但这不是重点。重点是表明编译器不会抱怨在函数调用中使用了未初始化的值。
                      【解决方案20】:

                      我认为没有真正的原因,尽管我没有看到它经常使用(P/Invoke 调用除外)。

                      【讨论】:

                        【解决方案21】:

                        .NET 之外的某些语言将它们用作无操作宏,仅向程序员提供有关如何在函数中使用参数的信息。

                        【讨论】:

                          【解决方案22】:

                          我认为主要原因是“......虽然返回值很常见并且被大量使用,但out ref参数的正确应用需要中间设计和编码技能。为普通观众设计的图书馆架构师不应该期望用户在没有 out 或 ref 参数的情况下掌握工作。”

                          请阅读更多内容:http://msdn.microsoft.com/en-us/library/ms182131.aspx

                          【讨论】:

                            猜你喜欢
                            • 2017-01-31
                            • 1970-01-01
                            • 1970-01-01
                            • 2021-08-23
                            • 1970-01-01
                            • 1970-01-01
                            • 1970-01-01
                            • 2011-11-29
                            • 2019-04-07
                            相关资源
                            最近更新 更多