August, 2006

  • The Old New Thing

    Even more about C# anonymous methods, from the source

    • 81 Comments

    If you want to know still more about C# anonymous methods, you can check out the web site of Grant Richins who has an entire category devoted to anonymous methods, and he should know, since he actually implemented them.

    Now that CLR week is over, I'm curious what you all thought of it. Would you like to see another CLR week at some point? Should I stick to Win32? (Or doesn't it matter because I'm an arrogant Microsoft apologist either way?)

  • The Old New Thing

    Applications and DLLs don't have privileges; users do

    • 75 Comments

    I can't believe you people are actually asking for backdoors. If an end user can do it, then so can a bad guy.

    In response to the requirement that all drivers on 64-bit Windows be signed, one commenter suggested adding a backdoor that permits unsigned drivers, using some "obscure registry key". Before somebody can jump up and shouts "security through obscurity!", the commenter adds this parenthetical: "(that no application has privileges to do by default)".

    What does that parenthetical mean? How do you protect a registry key from an application? And if applications don't have privileges to modify a key, then who does?

    The Windows security model is based on identity. Applications don't have privileges. Users have privileges. If an application is running in your user context, then it can do anything you can, and that includes setting that "obscure registry key". (This is a variation on "Your debugging code can be a security hole".) Same goes for DLLs. There's no such thing as something only an individual program/library can read/write to or do. You can't check the "identity of the calling library" because you can't trust the return address. Coming up with some other "magic encryption key" like the full path to the DLL won't help either, because a key that anybody can guess with 100% accuracy isn't much of a key.

    Yes, UNIX has setuid, but that still doesn't make applications security principals. Even in UNIX, permissions are assigned to users, not to applications.

    That's one of the reasons I get so puzzled when I hear people say, "Windows should let me do whatever I want with my system", while simultaneously saying, "Windows should have used ACLs to prevent applications from doing whatever they want with my system." But when you are running an application, the application is you. If you can do it, then an application can do it because the application is you.

    Some people want to extend the concept of security principal to a chunk of code. "This registry key can be written to only by this function." But how could you enforce this? Once you let untrusted code enter a process, you can't trust any return addresses any more. How else could you identify the caller, then?

    "Well, the DLL when it is created is given a magic cookie that it can use to prove its identity by passing that cookie to these 'super-secure functions'. For example,

    // SECRET.DLL - a DLL that protects a secret registry key
    HANDLE g_hMagicCookie;
    
    // this function is called by means to be determined;
    // it tells us the magic cookie to use to prove our identity.
    void SetMagicCookie(HANDLE hMagicCookie)
    {
     g_hMagicCookie = hMagicCookie;
    }
    

    and then the program can use the magic cookie to prove that it is the caller. For example, you could have RegSetValueWithCookie(g_hMagicCookie, hkey, ...), where passing the cookie means 'It's me calling, please give me access to that thing that only I have access to."

    That won't stop the bad guys for long. They just have to figure out where the DLL saves that cookie and read it, and bingo, they're now you.

    // bad-guy program
    
    int CALLBACK WinMain(...)
    {
     // call some random function from SECRET.DLL
     // so it gets loaded and the magic cookie gets
     // initialized.
     SomeFunctionFromSECRETDLL();
    
     // experimentation tells us that SECRET.DLL
     // keeps its magic cookie at address 0x70131970
     HANDLE hMagicCookie = *(HANDLE*)0x70131970;
     RegSetValueWithCookie(hMagicCookie, hkey, ...);
    
     return 0;
    }
    

    Ta-da, we now have a program that writes to that registry key that SECRET.DLL was trying to protect. It does it by merely waiting for SECRET.DLL to receive its magic cookie, then stealing that cookie.

    "Well, sure, but if I combine that with the check-the-return-address technique, then that'll stop them."

    No, that doesn't stop anybody. All the bad guy has to do is change the RegSetValueWithCookie(hMagicCookie, hkey, ...) to code that hunts for a trusted address inside SECRET.DLL and cooks up a fake stack so that when control reaches RegSetValueWithCookie, everything in memory looks just like a legitimate call to the function, except that the attacker got to pass different parameters.

    You can come up with whatever technique you want, it won't do any good. Once untrusted code has been granted access to a process, the entire process is compromised and you cannot trust it. Worst case, the attacker just sets a breakpoint on RegSetValueWithCookie, waits for the breakpoint to hit, then edits the stack to modify the parameters and resumes execution.

    That's why code is not a security principal.

    Corollary: Any security policy that says "Applications cannot do X without permission from the user" is flawed from conception. The application running as the user is the user. It's one thing to have this rule as a recommendation, even a logo requirement, but it's another thing to enforce this rule in the security subsystem.

  • The Old New Thing

    Try to avoid having BOOL function parameters

    • 66 Comments

    Generally speaking, I believe that you should try to avoid giving functions a boolean parameter (BOOL, bool, etc.) unless the meaning of that boolean parameter is blatantly obvious. Examples of obvious meaning would be the second parameter to the EnableWindow function (TRUE obviously means the window is being enabled and FALSE means that it's being disabled) and the final parameter to ShowScrollBar (TRUE obviously means the scroll bar is being shown and FALSE means that it's being hidden). In both of these cases, the meaning of the boolean parameter is encoded in the name of the function itself.

    But for functions like CreateEvent, what does that first BOOL parameter mean? First, you have to realize that the first BOOL parameter controls whether you get an auto-reset event or a manual-reset one. Does FALSE create a manual-reset event? Or is that done by passing TRUE? I can never remember and I have to go looking it up each time. That first parameter should have been declared as, say, a DWORD or, even better, an enum with two legal values, EVENTTYPE_AUTORESET and EVENTTYPE_MANUALRESET.

    Even worse is that CreateEvent has two BOOL parameters. Like anybody reading the code later can remember which comes first.

    And the mystery bools keep coming. Consider, for example, StreamReader(Stream, bool). What does true mean? Or false? Heck if I know.

    Mind you, this is just my opinion. Others may disagree with me.

  • The Old New Thing

    Who says there's only one? There can be more than one logon session

    • 60 Comments

    An extension of the "What if two programs did this?" thought experiment is the "Who says there's only one?" question.

    A common question I see is, "From a service, how do I do X with the currently logged-on user?" (Where "X" can be a variety of things such as interact with them or impersonate them.) But who says that there's only one?

    With the introduction of Fast User Switching in Windows XP, the possibility for multiple logged-on users exists even in consumer scenarios. You might say, "Well, I mean that among all the users that are logged on, I want the one that's using the computer right now." Except that with Media Center Extender, there can still be two users, one sitting at the console and another in the TV room with the extender, and both of them are using the computer right now. And on the server side of things, Terminal Services has been around since NT4 (a limited version of which is available in Windows XP Professional under the name "Remote Desktop").

    There are many variations on this question. "How can I check whether the 'Press Ctrl+Alt+Del to begin' dialog is being displayed?" There is not just one such dialog; there is one for every session. If there are three users logged onto the machine, one of them logged in and active, another one disconnected, and a third sitting at the 'Press Ctrl+Alt+Del to begin' dialog, what should the answer be? That depends on what you're planning to use this information for.

    Sometimes it takes a few iterations before people get the message, and sometimes they never do.

    "How do I impersonate the currently logged-on user?"

    "Which user? There can be more than one. <explanation>"

    "I want the one on the desktop."

    "They all have their own desktop."

    "I want the one on the current desktop."

    "What do you mean by 'the' current desktop? There is one current desktop for each user."

    "I want the desktop I should display my UI on."

    "Your question has become circular. You want to display your UI on the desktop that your UI should be displayed on."

    "Why won't you answer my question?!"

  • The Old New Thing

    The cultural anthropology of getting on a bicycle

    • 53 Comments

    I can tell where you grew up by watching you get on a bicycle.

    Well, sort of.

    In my limited experience, I've observed two distinct ways of getting on a bicycle. The first is what I'll call the Chinese method, since it's the dominant technique in China, Taiwan, and Japan, as far as I can tell. To get on a bicycle using the Chinese method, stand to the side of the bicycle (let's say, the left side) with both hands on the handlebars in the normal riding position. Put the left pedal at the bottom of its stroke and place your left foot on it. Now ride the bicycle like a scooter, pushing off with your right foot, until you pick up some speed. While coasting forward, swing your right leg over the back of the bicycle and place your right foot upon the right pedal (which is at the top of its stroke). Sit down and begin pedaling.

    The second method I will call the U.S. method since it is the dominant technique in the States. To get on a bicycle using this method, straddle the bicycle with both hands on the handlebars in the normal riding position, placing one foot on the ground and the other foot on its corresponding pedal at the top of its stroke. Simultaneously push forward with the ground foot and put all your weight on the pedal foot (driving it forward). This will get you coasting very slowly. Use this time to take the foot that was on the ground and place it on its corresponding pedal (which by now has reached the top of its stroke). Sit down and begin pedaling.

    Each of these methods has its own drawbacks. The U.S. method requires you to be able to maintain your balance on a bicycle at very low speeds and then get your ground foot into position quickly before you run out of momentum. (This is particularly tricky when you have to strap or clip your foot into place.) On the other hand, the Chinese method requires you to shift your weight while balancing on a single foot. That bit is my downfall. When all my weight gets on that one foot, I start wobbling and can't quite finish the move.

    Now, using this method to determine where someone learned to ride a bicycle is not foolproof, of course. Some of my childhood friends who grew up in the United States to Taiwanese parents use the Chinese method.

    During my brief travels in Europe, I neglected to take note of how the locals got on their bicycles. Maybe there's a European way of getting on a bicycle?

  • The Old New Thing

    The implementation of anonymous methods in C# and its consequences (part 1)

    • 50 Comments

    You may not even have realized that there are two types of anonymous methods. I'll call them the easy kind and the hard kind, not because they're actually easy and hard for you the programmer, but because they are easy and hard for the compiler.

    The easy kind is the anonymous method that doesn't use any local variables from its lexically-enclosing method. These are anonymous methods that could have been their own separate member functions; all the anonymization does is save you the trouble of coming up with names for them:

    class MyClass1 {
     int v = 0;
     delegate void MyDelegate(string s);
    
     MyDelegate MemberFunc()
     {
      int i = 1;
      return delegate(string s) {
              System.Console.WriteLine(s);
             };
      }
    }
    

    This particular anonymous method doesn't access any MyClass1 members, nor does it access the local variables of the MemberFunc function; therefore, it can be converted to a static method of the MyClass1 class:

    class MyClass1_converted {
     int v = 0;
     delegate void MyDelegate(string s);
    
     // Autogenerated by the compiler
     static void __AnonymousMethod$0(string s)
     {
      System.Console.WriteLine(s);
     }
    
     MyDelegate MemberFunc()
     {
      int i = 1;
      return __AnonymousMethod$0;
      // which is in turn shorthand for
      // return new MyDelegate(MyClass1.__AnonymousMethod$0);
      }
    }
    

    All the compiler did was give your anonymous methods a name and use that name in place of the "delegate (...) { ... }". (Note that all compiler-generated names I use here are purely illustrative. The actual compiler-generated name will be something different.)

    On the other hand, if your anonymous method used the this parameter, then that makes it an instance method instead of a static method:

    class MyClass2 {
     int v = 0;
     delegate void MyDelegate(string s);
    
     MyDelegate MemberFunc()
     {
      int i = 1;
      return delegate(string s) {
              System.Console.WriteLine("{0} {1}", v, s);
             };
      }
    }
    

    The anonymous method in MyClass2 uses the this keyword implicitly (to access the member variable v). Therefore, the conversion is to an instance member rather than to a static member.

    class MyClass2_converted {
     int v = 0;
     delegate void MyDelegate(string s);
    
     // Autogenerated by the compiler
     void __AnonymousMethod$0(string s)
     {
      System.Console.WriteLine("{0} {1}", v, s);
     }
    
     MyDelegate MemberFunc()
     {
      int i = 1;
      return this.__AnonymousMethod$0;
      // which is in turn shorthand for
      // return new MyDelegate(this.__AnonymousMethod$0);
      }
    }
    

    So far, we've only dealt with the easy cases. The transformation is local and not particularly complicated. These are the sorts of transformations you could make yourself without too much difficulty in the absence of anonymous methods.

    The hard case is where things get interesting. The body of an anonymous method is permitted to access the local variables of its lexically-enclosing method, in which case the compiler needs to keep those variables alive so that the body of your anonymous method can access them. Here's a sample anonymous method that accesses local variables from its lexically-enclosing method:

    class MyClass3 {
     int v = 0;
     delegate void MyDelegate(string s);
    
     MyDelegate MemberFunc()
     {
      int i = 1;
      return delegate(string s) {
              System.Console.WriteLine("{0} {1} {2}", i++, v, s);
             };
      }
    }
    

    In this example, the anonymous method prints "1 v s" the first time it is called, then "2 v s" the second time it is called, and so on, with the integer increasing by one. (And where v s are the current values of v and s, of course.) This happens because the i variable that the anonymous method is accessing is the same one each time, and it's the same i that the MemberFunc method was using, too. If the function were rewritten as

    class MyClass4 {
     int v = 0;
     delegate void MyDelegate(string s);
    
     MyDelegate MemberFunc()
     {
      int i = 0;
      MyDelegate d = delegate(string s) {
              System.Console.WriteLine("{0} {1} {2}", i++, v, s);
             };
      i = 1;
      return d;
      }
    }
    

    the behavior would be the same as in MyClass3. The creation of the delegate from the anonymous method does not make a copy of the i variable; changes to the i variable in the MemberFunc are visible to the anonymous method because both are accessing the same variable.

    When faced with this "hard" type of anonymous method, wherein variables are shared with the lexically-enclosing method, the compiler generates a helper class:

    class MyClass3_converted {
     int v = 0;
     delegate void MyDelegate(string s);
    
     // Autogenerated by the compiler
     class __AnonymousClass$0 {
      MyClass this$0;
      int i;
      public void __AnonymousMethod$0(string s)
      {
        System.Console.WriteLine("{0} {1} {2}", i++, this$0.v, s);
      }
     }
    
     MyDelegate MemberFunc()
     {
      __AnonymousClass$0 locals$ = new __AnonymousClass$0();
      locals$.this$0 = this;
      locals$.i = 0;
      return locals$.__AnonymousMethod$0;
      // which is in turn shorthand for
      // return new MyDelegate(locals$.__AnonymousMethod$0);
      }
    }
    

    Wow, there was a lot of rewriting this time. A helper class was created to contain the local variables that were shared between the MemberFunc function and the anonymous method (in this case, just the variable i), as well as the hidden this parameter (which I have called this$). In the MemberFunc function, access to that shared variable is done through this anonymous class, and the anonymous method that you wrote is an anonymous method on the anonymous class.

    Notice that the assignment to i in MemberFunc modifies the copy inside locals$, which is the same object that the anonymous method will be using when it runs. That's why it prints "1 v s" the first time: The value had already been changed to 1 by the time the delegate ran for the first time.

    Those who have done a good amount of C++ programming (or C# 1.0 programming) are well familiar with this technique, since C++ callbacks typically are given only one context variable; that context variable is usually a pointer to a larger structure that contains all the complex context you really want to operate on. C# 1.0 programmers went through a similar exercise. The "hard" type of anonymous method provides syntactic sugar that saves you the hassle of having to declare and manage the helper class.

    If you thought about it some, you'd have realized that the way it's done is pretty much the only way it could have been done. It turns out that most computer programming doesn't consist of being clever or making hard decisions. You just have one kernel of an idea ("hey let's have anonymous methods") and then the rest is just doing what has to be done, no actual decisions needed. You just do the obvious thing. Most programming consists of just doing the obvious thing.

    Okay, so that's a quick introduction to the implementation of anonymous methods in C#. Mind you, this information isn't just for your personal edification. It's actually important that you understand how these works (and not just treat it as "magic"), because lack of said understanding can lead to subtle programming errors. We'll look at those types of errors over the next few days.

  • The Old New Thing

    A modest proposal: On allowing mobile phones on airplanes

    • 48 Comments

    Ever since the FAA decided to reconsider its ban on the use of cellular phones on airplanes during flight, there has been quite a reaction over whether this is a good thing.

    To resolve this issue, I present this modest proposal.

    Remember back in the days when smoking was permitted on airplanes? When you bought a ticket, you were asked whether you wanted to be in the smoking or non-smoking section. We can do the same thing with mobile phones. You can ask to be seated in the "yapping" or "non-yapping" section. If you're in the yapping section, then yap all you want. Call your grandmother and talk with her the entire flight about her colonoscopy, see if I care. On the other hand, in the non-yapping section, the phone stays off.

    This principle of yap-or-no-yap can be extended to other in-flight disturbances. The no-yapping section would be renamed the "quiet section", where electronic devices must be muted (or used with headphones) and conversations must take place at low volume.

    (Hey, it worked for Amtrak, it can work on airplanes.)

    Side note: I wonder if the temporary ban on carry-on luggage in the UK improved airplane boarding times. Perhaps we could ban carry-on luggage across the board, not as a security measure, but as an efficiency. I don't know about you, but I'm really anonyed by those people whose "one carry-on bag and one personal item" consist of a carry-on bag the size of a small refrigerator and a personal item the size of a microwave oven. They clog up the aisle for ages trying to heave both of their bags into the overhead bin (even though the personal item is supposed to go under the seat in front of you). Okay, enough ranting.

    Next time (if there is a next time), I'll solve the problem of steroids in baseball.

  • The Old New Thing

    Exploiting the inattentive, episode 2: The unlabeled line

    • 42 Comments

    In Episode 1 we learned that "Snide" brand powdered laundry detergent comes with a scoop that holds 5/8 cup detergent, even though the directions call for only 3/8 cup. The intent here is to get people to use more detergent than necessary, thereby increasing sales. A box of detergent which should be sufficient for 30 loads ends up running out after only 18. Consider it "technically legal" mislabeling.

    I returned to Snide brand powdered laundry detergent this week and discovered that they have taken this little trick to a new level. The markings on the side of the scoop read like this:

      2  
      1  
         
         

    The accompanying instructions give these instructions for how much detergent to use:

    Fill to bottom line  (3/8 cup)  for normal loads.
    Fill to line 1  (1/2 cup)  for large loads.
    Fill to line 2  (3/4 cup)  for extra-soiled loads.

    Notice that the amount of detergent you're supposed to use on a regular basis is indicated by the bottom unmarked line, the one you will barely notice. Meanwhile, if you're inattentive and use the entire scoop, you've used one cup of detergent, over twice as much as you were intending.

    My prediction: The next time I buy Snide brand powdered laundry detergent, the scoop will be 1½ cups, with an unlabeled line at 1/2 cup, another line at 3/4 cup labeled "1" and a third line at 1 cup labeled "2". The instructions will go something like this:

    Fill halfway to line 1  (3/8 cup)  for normal loads.
    Fill to bottom line  (1/2 cup)  for large loads.
    Fill to line 1  (3/4 cup)  for extra-soiled loads.
  • The Old New Thing

    Moving a file does not recalculate inherited permissions

    • 39 Comments

    Inherited permissions on an object are established when it is created. Once the object has been created, you can change the permissions of the parent and it won't have any effect unless you explicitly ask for the inheritable properties to be re-propagated to the child objects. (You may recall that the CREATOR_OWNER SID works in a similar way.) This rule applies to files, though that can lead to behavior that some people might consider non-intuitive.

    Files are strange from a security perspective because a single file can have multiple parent folders, thanks to hard links. Suppose you have two directories DirA and DirB. DirA has an inheritable permission that gives UserA full access and denies access to UserB, while DirB does the same with the roles of the two users reversed. It's easy to tell who the parent of a file is when it is created, since a file is created by giving a path, and you can extract the parent from the path. For example, if the file DirA\File is created, it will naturally inherit permissions from DirA.

    One the file is created, though, that's the end of it. Inheritable permissions don't have any effect any more.

    This simple rule wouldn't normally cause any problems, except that files have properties unusual among most named objects: They can go by multiple names (thanks to hard links) and can change their names (via renaming). The apparently counter-intuitive behavior stems from confusing the object with its name.

    For example, suppose we create a hard link to DirA\File under the name DirB\File. What effect does this have on the file's ACLs? Answer: None whatsoever. The inheritable ACLs on DirB aren't applied to the file since the file is not being created. Besides, you couldn't apply the inheritable ACLs from DirB even if you wanted to because it would create a paradox: The file resides in both DirA and DirB, but each of those directories contains contradictory inheritable permissions. What would the result be if you somehow managed to apply both of them?

    All right, then. We have no choice but to decide that a file's ACLs don't change when you create a hard link. Now delete the original link DirA\File, leaving just DirB\File. Does this change the ACLs now? If you believe that it should, then you're saying that inheritable ACLs can take effect even when nothing got created! After all, we didn't create a hard link; we deleted one.

    Okay, maybe you concede that deleting a hard link shouldn't affect the ACL. But what did we just do by creating a hard link and then deleting another one? The net effect is that we moved the file from DirA\File to DirB\File. Which brings us to our third example: Renaming/moving a file does not change its ACL.

    We've just rediscovered the simple rule that inheritable ACLs take effect only when a file is created. Nothing special happens when a new hard link is created or when the file is moved.

    Of course, that simple rule holds only when you look at the file system at a low level. Layers built on top of the low-level file system can end up complicating our simple rule.

    When you move a file across volumes with the MOVEFILE_COPY_ALLOWED flag, you're saying that "move the file if possible; if not, then convert it to a copy/delete operation". The copy operation creates a new file, which means that inheritable properties on the destination folder do take effect. But only if the file motion crosses volumes. If you're moving the file within the same volume, then the ACL remains unchanged. How confusing. When you pass the MOVEFILE_COPY_ALLOWED flag, you lose control of the ACL. (You actually lose control of much more than just the ACL. Since the file is being copied, none of the attributes from the original file are kept on the copy. The copy inherits its encryption and compression status from the new parent directory. The copy also gets a new owner, which has follow-on consequences for things like disk quota.)

    Another layer built on top of the low-level file system operations is the shell's copy engine. If you use SHFileOperation to move a file and pass the FOF_NOCOPYSECURITYATTRIBUTES flag, then it will not preserve the original ACLs on the moved files but will rather recalculate them from the destination's inheritable properties. (If you want to do the same thing in your own code, you can call the SetNamedSecurityInfo function, specifying that you want an empty, unprotected DACL.) In Windows XP, the shell decides whether or not to use this "naive mode" ACL management based on the following algorithm:

    • If the MoveSecurityAttributes policy is set, then the policy determines how ACLs are handled when files are moved. (ACLs are reset if set to "0" and are preserved if set to "1".)
    • Otherwise, the "Use simple file sharing" setting controls how ACLs are handled when files are moved. (ACLs are reset if simple file sharing is enabled and are preserved if simple file sharing is disabled.)
  • The Old New Thing

    The implementation of anonymous methods in C# and its consequences (part 3)

    • 38 Comments

    Last time we saw how the implementation details of anonymous methods can make themselves visible when you start taking a delegate apart by looking at its Target and Method. This time, we'll see how an innocuous code change can result in disaster due to anonymous methods.

    Occasionally, I see people arguing over where local variables should be declared. The "decentralists" believe that variables should be declared as close to their point of first use as possible:

    void MyFunc1()
    {
     ...
     for (int i = 0; i < 10; i++) {
      string s = i.ToString();
      ...
     }
     ...
    }
    

    On the other hand, the "consolidators" believe that local variables should be declared outside of loops.

    void MyFunc2()
    {
     ...
     string s;
     for (int i = 0; i < 10; i++) {
      s = i.ToString();
      ...
     }
     ...
    }
    

    The "consolidators" argue that hoisting the variable s means that the compiler only has to create the variable once, at function entry, rather than each time through the loop.

    As a result, you can find yourself caught in a struggle between the "decentralists" and the "consolidators" as members of each school touch a piece of code and "fix" the local variable declarations to suit their style.

    And then there are the "peacemakers" who step in and say, "Look, it doesn't matter. Can't we all just get along?"

    While I admire the desire to have everyone get along, the claim that it doesn't matter is unfortunately not always true. Let's stick some nasty code in where the dots are:

    delegate void MyDelegate();
    void MyFunc1()
    {
     MyDelegate d = null;
     for (int i = 0; i < 10; i++) {
      string s = i.ToString();
      d += delegate() {
       System.Console.WriteLine(s);
      };
     }
     d();
    }
    

    Since the s variable is declared inside the loop, each iteration of the loop gets its own copy of s, which means that each delegate gets its own copy of s. The first time through the loop, an s is created with the value "0" and that s is used by the first delegate. The second time through the loop, a new s is created with the value "1", and that new s is used by the second delegate. The result of this code fragment is ten delegates, each of which prints a different number from 0 to 9.

    Now, a "consolidator" looks at this code and says, "How inefficient, creating a new s each time through the loop. I shall hoist it and bask in the accolades of my countrymen."

    delegate void MyDelegate();
    void MyFunc2()
    {
     MyDelegate d = null;
     string s;
     for (int i = 0; i < 10; i++) {
      s = i.ToString();
      d += delegate() {
       System.Console.WriteLine(s);
      };
     }
     d();
    }
    

    If you run this fragment, you get different behavior. A single s variable is created for all the loop iterations to share. The first time through the loop, the value of s is "0", and then the first delegate is created. The second loop iteration changes the value of s to "1" before creating the second delegate. Repeat for the remaining eight delegates, and at the end of the loop, the value of s is "9", and ten delegates have been added to d. When d is invoked, all the delegates print the value of the s variable, which they are sharing and which has the value "9". The result: 9 is printed ten times.

    Now, I happen to have constructed this scenario to make the "consolidators" look bad, but I could also have written it to make the "decentralists" look bad for pushing a variable declaration into a loop scope when it should have remained outside. (All you have to do is read the above scenario in reverse.)

    The point of this little exercise is that when a "consolidator" or a "decentralist" goes through an entire program "tuning up" the declarations of local variables, the result can be a broken program, even though the person making the change was convinced that their change "had no effect; I was just making the code prettier / more efficient".

    What's the conclusion here?

    Write what you mean and mean what you write. If the precise scope of a variable is important, make sure to comment it as such so that somebody won't mess it up in a "clean-up" pass over your program. If there are two ways of writing the same thing, then write the one that is more maintainable. And if you feel that one method is superior from a performance point of view, then (1) make sure you're right, and (2) make sure it matters.

Page 1 of 4 (39 items) 1234