Design Guidelines Update: Variable Number of Arguments


style="FONT-WEIGHT: normal; COLOR: windowtext; FONT-FAMILY: Arial"> size=2>Based on some very good feedback from href="http://blogs.gotdotnet.com/EricGu/">Eric
Gunnerson
, I recently updated the Design Guidelines with
some more info on Params.


style="FONT-WEIGHT: normal; COLOR: windowtext; FONT-FAMILY: Arial"> size=2>As always, comments welcome.


style="FONT-WEIGHT: normal; COLOR: windowtext; FONT-FAMILY: Arial"> size=2> 


style="FONT-WEIGHT: normal; COLOR: windowtext; FONT-FAMILY: Arial"> size=2>Have a guideline that is consistent with the current set that you’d like
to see added?  Write it up in this
format and I will see what I can do.


style="FONT-WEIGHT: normal; COLOR: windowtext; FONT-FAMILY: Arial"> size=2> 


style="FONT-FAMILY: Arial"> size=2>  size=5>Variable Number of Arguments title="" style="mso-footnote-id: ftn1" href="#_ftn1" name=_ftnref1> style="mso-bookmark: _Toc53308623"> style="mso-special-character: footnote"> style="FONT-SIZE: 14pt; FONT-FAMILY: Verdana; mso-fareast-font-family: 'Times New Roman'; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt; mso-font-kerning: 12.0pt; mso-ansi-language: EN-US; mso-bidi-language: AR-SA; mso-fareast-language: EN-US">[1] style="mso-bookmark: _Toc53308623">


face=Verdana>Methods that can take an unlimited number of
parameters are expressed by providing an overload that takes an array. For
example, String.Format could provides the following
overload:


style="mso-bookmark: OLE_LINK86">string
String.Format(string format, object[] args)


style="mso-bookmark: OLE_LINK86">A user can then
write:


style="mso-bookmark: OLE_LINK86"> size=2>    String.Format(“{0} {1} {2} {3}”, new object[] {1, 2,
3, 4});


style="mso-bookmark: OLE_LINK86">Adding the
params keyword to the last parameter:


style="mso-bookmark: OLE_LINK86"> size=2>    string String.Format(string format, params object[]
args)


style="mso-bookmark: OLE_LINK86">provides a
shortcut to creating the temporary array, and allows the user to
write:


style="mso-bookmark: OLE_LINK86"> size=2>    String.Format(“{0} {1} {2} {3}”, 1, 2, 3,
4);


style="mso-bookmark: OLE_LINK86">Users expect to
use the short form, and it’s therefore important to use “params” where
appropriate.


style="MARGIN: 3pt 0in 3pt 0.25in; TEXT-INDENT: -0.25in"> style="mso-bookmark: OLE_LINK86"> style="mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana"> style="mso-list: Ignore"> style="FONT: 7pt 'Times New Roman'">           
Don’t use params if the user
would not expect to use the short form. Consider:

   
face="Courier New"> style="FONT-SIZE: 9pt; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt">public
void Write(byte[] value);


face=Verdana>In this case, the user would not expect to be able to specify
individual bytes, so params would not be
appropriate.


style="mso-bookmark: OLE_LINK86"> style="FONT-FAMILY: Symbol; mso-fareast-font-family: Symbol; mso-bidi-font-family: Symbol"> style="mso-list: Ignore">*<br src="file:///D:\DOCUME~1\brada\LOCALS~1\Temp\msohtml1\09\clip_image002.gif"
width=13>
style="FONT: 7pt 'Times New Roman'">     
Do use params if all overloads
are performing the identical operation. For example:

   
face="Courier New"> style="FONT-SIZE: 9pt; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt">public
void Combine(Delegate delegate1, Delegate
delegate2);
   public void Combine(Delegate[]
delegates);

Adding
params to the second overload allows the use the same idiom in their code
regardless of the number of arguments.


style="mso-bookmark: OLE_LINK86"> size=2> 


style="mso-bookmark: OLE_LINK86"> style="FONT-FAMILY: Symbol; mso-fareast-font-family: Symbol; mso-bidi-font-family: Symbol"> style="mso-list: Ignore">*<br src="file:///D:\DOCUME~1\brada\LOCALS~1\Temp\msohtml1\09\clip_image002.gif"
width=13>
style="FONT: 7pt 'Times New Roman'">     
Consider using Params if a simple
overload could use params but a more complex one could not, ask yourself “would
users value the utility of having params on one overload even if it wasn’t on
all overloads”. Consider the following overloaded
methods:
face="Courier New"> style="FONT-SIZE: 9pt; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt">       
ExecuteAssembly(String,Evidence,String[])
       
ExecuteAssembly(String,Evidence,String[],Byte[],AssemblyHashAlgorithm)

face=Verdana size=2>Even if the parameters could be re-ordered in the second
overload to place an array as the last parameter, because there are two arrays,
it’s not a good candidate to use params. But it could be used on the first
overload. If the first overload is often used, users will appreciate the
simplification of using params. The correct params usage in this case
is:
class=codeChar1> style="FONT-SIZE: 9pt; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt"> face="Courier New">public int ExecuteAssembly(string assemblyFile, Evidence
assemblySecurity, params string[]
args);
style="mso-bookmark: OLE_LINK86"> style="mso-bidi-language: HE; mso-no-proof: yes">public int
ExecuteAssembly(string style="mso-bidi-font-style: italic">assemblyFile, Evidence style="mso-bidi-font-style: italic">assemblySecurity, string[] style="mso-bidi-font-style: italic">args, byte[] style="mso-bidi-font-style: italic">hashValue, AssemblyHashAlgorithm
hashAlgorithm);

A
simpler example is the following:
style="mso-bookmark: OLE_LINK86"> class=codeChar1> style="FONT-SIZE: 9pt; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt">       
FillPolygon(Brush,PointF[])
       
FillPolygon(Brush,PointF[],FillMode)
face=Verdana>The user will want “params” on the first version, event though they
can’t use it on the second version. The correct params usage in this case
is:


style="mso-bookmark: OLE_LINK86"> style="FONT-WEIGHT: normal; FONT-SIZE: 9pt; COLOR: windowtext; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt">       public
void FillPolygon(Brush brush,
style="mso-bookmark: OLE_LINK86"> style="FONT-SIZE: 9pt; COLOR: windowtext; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt">params
face="Courier New"> class=codeChar1> style="FONT-WEIGHT: normal; FONT-SIZE: 9pt; COLOR: windowtext; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt">
PointF[] points)
       public void
FillPolygon(Brush brush,PointF[] points,FillMode fillmode) style="mso-special-character: line-break"> style="mso-special-character: line-break">
style="mso-bookmark: OLE_LINK86"> style="FONT-WEIGHT: normal; COLOR: windowtext">


style="mso-bookmark: OLE_LINK86"> style="FONT-FAMILY: Symbol; mso-fareast-font-family: Symbol; mso-bidi-font-family: Symbol"> style="mso-list: Ignore">*<br src="file:///D:\DOCUME~1\brada\LOCALS~1\Temp\msohtml1\09\clip_image002.gif"
width=13>
style="FONT: 7pt 'Times New Roman'">     
Do order parameters so that it’s
possible to apply “params” to the methods. Consider the
following:

face="Courier New"> style="FONT-SIZE: 9pt; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt">       
Find(IComparer)
       
Find(String[],IComparer)
       
Find(String[])

Because of
the ordering of parameters on the second overload, the opportunity to use
“params” has been lost. If this had been written as:

style="mso-bookmark: OLE_LINK86"> class=codeChar1> style="FONT-SIZE: 9pt; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 10.0pt">       
Find(IComparer)
        Find(IComparer,
String[])
       
Find(String[])

face=Verdana>“params” could have been used for both
overloads.


style="mso-bookmark: OLE_LINK86"> size=2> 


style="MARGIN: 3pt 0in 3pt 0.25in; mso-list: l1 level1 lfo1"> style="mso-bookmark: OLE_LINK86"> style="FONT-FAMILY: Symbol; mso-fareast-font-family: Symbol; mso-bidi-font-family: Symbol"> style="mso-list: Ignore">*<br src="file:///D:\DOCUME~1\brada\LOCALS~1\Temp\msohtml1\09\clip_image001.gif"
width=12>
style="FONT: 7pt 'Times New Roman'">     
Do use the
params construct instead of several overloaded methods for repeated
arguments
href="#_ftn2" name=_ftnref2> class=MsoFootnoteReference> class=MsoFootnoteReference> style="FONT-SIZE: 10pt; FONT-FAMILY: Verdana; mso-fareast-font-family: 'Times New Roman'; mso-bidi-font-family: 'Times New Roman'; mso-ansi-language: EN-US; mso-bidi-language: AR-SA; mso-fareast-language: EN-US">[2] style="mso-bookmark: OLE_LINK86"> face=Verdana>.

style="mso-bookmark: OLE_LINK86">

style="MARGIN: 3pt 0in 3pt 0.25in; mso-list: l1 level1 lfo1"> style="FONT-FAMILY: Symbol; mso-fareast-font-family: Symbol; mso-bidi-font-family: Symbol"> style="mso-list: Ignore">*<br src="file:///D:\DOCUME~1\brada\LOCALS~1\Temp\msohtml1\09\clip_image001.gif"
width=12>
style="FONT: 7pt 'Times New Roman'">     
Do add the params
keyword to a parameter that meets these guidelines even if you have already
shipped it once undecorated.  Adding
params is NOT a breaking
change.  Existing client code will
continue to work as expected and new code can start to take advantage of style="mso-bidi-font-weight: normal">params.


style="MARGIN: 3pt 0in 3pt 0.25in; TEXT-INDENT: -0.25in"> style="mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana"> style="mso-list: Ignore"> style="FONT: 7pt 'Times New Roman'">           
Do not use the class=Bold>params style="FONT-WEIGHT: normal">when the array can is modified by the method.
Because the array is a temporary any modifications to the array will be
lost.


style="MARGIN: 3pt 0in 3pt 0.25in; TEXT-INDENT: -0.25in"> style="mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana"> style="mso-list: Ignore"> style="FONT: 7pt 'Times New Roman'">           
Do not use the class=Bold>VarArgs calling convention, otherwise known
as the ellipsis (…), exclusively because the Common Language Specification does
not support it
href="#_ftn3" name=_ftnref3> style="mso-special-character: footnote"> style="FONT-SIZE: 10pt; FONT-FAMILY: Verdana; mso-fareast-font-family: 'Times New Roman'; mso-bidi-font-family: 'Times New Roman'; mso-ansi-language: EN-US; mso-bidi-language: AR-SA; mso-fareast-language: EN-US">[3] face=Verdana>.


size=2>For extremely performance sensitive code, you might want to provide
special code paths for a small number of elements. style="mso-spacerun: yes">  You should only do this if you are going
to special case the entire code path (not just create an array and call the more
general method).  In such cases, we
recommend the following pattern as a balance between performance and the cost of
specially cased code.


style="MARGIN: 6pt 0in 0pt 0.25in; mso-add-space: auto"> face="Courier New">void Format (string formatString, object
arg1)
void Format (string formatString, object arg1, object
arg2)


style="MARGIN: 0in 0in 6pt 0.25in; mso-add-space: auto"> face="Courier New">
void Format (string formatString, params
object [] args)


style="COLOR: windowtext; FONT-FAMILY: Arial"> size=2> 









Comments (7)

  1. Pete says:

    Great stuff.

    So how come winforms "Control.ControlCollection.AddRange( Control [] controls )" doesn’t use params? It would look so much better.

  2. Brad Abrams says:

    Good point Pete… I just got mai from the Windows Forms team… they said they are fixing this and many other places params should be used… Look for them in Whidbey…

  3. S N says:

    Though, I thought of using this whereever it is appropriate, still I am restrained from using it for the following reason.

    The current versions of C# compiler doesn’t generate a proper IL code for this case.

    Issues:
    1. Everytime, you invoke a function that takes variable number of parameters using params,
    the generated IL code creates an object array to pack the values.
    Cost: An array object creation with clearing the allocated space.

    Image the cost if the function invokation is within a loop.

    2. The newly created temporary array of objects is stored in a new temporary variable created for this by the compiler.
    This itself is not a problem. But, a new temporary variable is created for each occurrence of the functions invokation that takes params argument. That is to say that if you are having 10 function invokation in a function then the containing function will have 10 temporary variable declared in it by the compiler to store the temporary variables.

    Problem caused by the above implementation:
    a. Since the JIT will not do proper register allocaton if the number of variables declared inside a IL function exceeds 64, the function will start suffering performance degradation. The developer of the code will hardly realize this issue.
    b. The amount of stack space also would be wasted (But minor issue though).

    3. After the function invokation completes its call, the temporary variable will never be cleared to null thereby retainging the life-time for those objects till the current functions finishes it job.

    Though, it may not be a fatal for most applications, it might be a fatal one if the callee is something like Main() function and one of those objects are critical resource.

    Solution:
    1. The C# compiler should clear the temporary variable created to store this array of objects immediately after the function calling.
    2. The compiler should declare maximum of only one temporary variable for this purpose per function and this same variable should be reused in that function in all occurrences.
    3. If the function invokation is within a loop, then the compiler should create the array outside the loop provided the loop is simple. (Loop unraveling).

    Since all the issues are related with how the C# compiler generates the code, it can be easily fixed. This pit fall also should be documented clearly for all C# compiler writers.

    Also, since the recommendation came from Eric G, you should be able to talk to him to get it fixed as soon as possible.

    Thanks
    Subbu

  4. Eric Gunnerson says:

    Subbu,

    A couple of comments on what you wrote.

    You are correct that the compiler creates a separate instance of an array and sets each of the elements each time. It could conceivably create one instance and then cache it for use on subsequent calls, but for this to work always, it would require that the lifetime of the temporary array did not extend beyond the method call.

    Unfortunately, there’s no restriction to what the method can do with that array – it could easily stash it away for later use – and therefore the compiler can’t safely reuse the same instance – it needs to create a new instance for each call. You may be able to do this optimization yourself if you know the behavior of the called method, but the compiler has no way of knowing, and therefore must assume the worst. One could conceive of a system where this information was available to the compiler, but I don’t think that this issue is where I would choose to spend my performance $$$.

    On the subject of performance, there is a penalty when you pass an array rather than individual parameters. For APIs where performance is important, it’s not uncommon to provide overloads for one parameter, two parameters, three parameters, and then an array of parameters to make the common case faster. Or, you can choose not to use an array of args at all, and simple create overloads for all methods, though this poses a code size penalty, which is ultimately a performance issue itself.

    Eric

  5. S N says:

    Hello Eric,

    I correct myself. After your posting, I refered the C# Language specification which states that the user can send his/her own array instead of asking the compiler to create one.

    But still, the C# compiler should correct its behavior on not clearing the temporary variable that it created after the function call.

    I am not asking the entire array needs to be cleared. All I am asking is that the temporary array variable needs to be set to null so that the array instance can be garbage collected if required.

    Thanks again for pointing my short comings
    Subbu

  6. S N says:

    This remainds me one more thing.

    The FxCop tool should add one more warnings under performance category.

    If the IL function has more than 64 variables, it should throw a warning so that the user can optimize the number of variables used in that function.

    When it throws the above warning, it should also say how many of them are created by the compiler so that the user has more information.

    Caution: The C# compiler could have created a temorary variable for genuine cases like multiple declaraion of same variable.

    Sample
    void fn()
    {
    for (int i = 0; i < 10; i++);

    for (int i = 0; i < 10; i++);
    }

    Subbu

  7. Frank Hileman says:

    Regarding AddRange(params ElementType []): we made this change to our AddRange function for a collection of IComponent types edited in a desginer (just like Controls), and it was a breaking change: it broke the code deserializer in visual studio. So it should not be recommended for this purpose until the visual studio code deserailizer is fixed to handle these params arg lists. The error is "Missing Method exception". Apparently it no longer matches the correct signature using reflection.

    Perhaps Brian Pepin or whoever works on that can be notified. Thanks.