|
|
|
The ATM project at the end of the previous unit was interesting, but it got kind of complicated looking, didn't it? It's not that there was anything mind-blowing going on in there, but with those "if" statements within "switch" statements within "while" loops within "try/catch" statements, things were starting to look a little "out of control". In this lesson, you're finally going to learn how to break code down into manageable blocks called "Methods". So far, your entire program has been contained in just one method, called "Main", under "Class1". But we can actually create as many methods as we want (we can create more classes, too... but that opens up a huge new area that we're not quite ready to cover yet). Here's a simple example of what adding a second method would look like, in Class1: class Class1 { [STAThread] static void Main(string[] args) { } static void PrintLine() { Console.WriteLine("---------------------------------------------------"); } } That first line where the method begins is called the method's signature. And as you can see, the signature of our new method, PrintLine(), looks a lot like the signature of Main(). The name is different, and there's nothing in the brackets at the end, but that's about it. You'll also notice that PrintLine() doesn't have a "[STAThread]" above it. This is because you only put that above the method your program starts in. Just like "if" statements, "for" loops, and any of the other control structures we used before, you can put any code you want inside the method. That includes "if" statements, and all the other stuff you learned in the last unit. When you're ready to call your method from Main(), you simply do this: PrintLine(); // Run the code in the PrintLine() method Perhaps the most obvious benefit from methods is code reuse. At any point in Main(), all we need to do is type PrintLine(), and our program will immediately run all of the code in that method. This is known as "calling" the method, and it simply means that we've run through the method's code. Another benefit is readability. By using methods to break our program into smaller chunks, we can "group" the code into small, logical components, based on the functionality they provide. You can also call methods from other methods! Just make sure that you don't go too deep into this; if a method calls a method which calls a method which calls a method (repeat dozens and dozens of times), you'll eventually get a stack overflow exception and your program will crash. That shouldn't be a shortcoming, however, because method calls should never have to go that deep to begin with. For a program of any decent size, using methods to break your code into smaller chunks is an inevitable step. However, exactly how far you go in breaking down your code is up to you. As a general rule of thumb, you should start looking for ways to break a method into smaller pieces when it contains more than 15 or 20 lines. Ideally, every method in your program, including Main(), should be short enough to fit on your screen. It's not always possible or practical to do this with every method (what you're really going for is logical groupings, not arbitrary line counts), but it's generally a good standard to follow. Giving good names to our methods is very important, because it actually allows us to create a "language" of our own, so that the code in the Main() method reads almost like English. For example, a program with well-named, logically grouped methods might read like this: GetUserName(); TranslateNameToPigLatin(); DisplayPigLatinName(); GetTwoNumbers(); AddNumbers(); DisplayResult(); ...and so on. Avoid using generic (a term I use in favor of "stupid") names, like GetData() or DoStuff(). No one ever believes this when they first start coding, but you will eventually forget how specific parts of your program work, especially if you leave it alone for a few weeks. Using specific, intelligent method names like DrawCard(), PurgeLogs() and DeleteHardDrive() will go a long way toward helping you remember what the code is actually doing. How about variables? We need a way to pass information between these methods, or they're going to be pretty limited in what they can do. Fortunately, it's pretty simple to provide information to a method. Here's how you would call and write a method that accepts a string variable: [STAThread] static void Main(string[] args) { PrintName("Frank"); } static void PrintName(string name) { Console.WriteLine("Your name is " + name); } Remember how you declare a "temporary" variable inside a "for" loop (in most cases, it was called "i")? Variables that you "pass" into a method work along similar lines. It's simply a name for the information that the calling method has provided. Here, it happens to be a string variable that contains a name, so the obvious thing to call the temporary variable is "name". In this case, we passed PrintName() a hard-coded string with the value "Frank", but of course you can pass in an actual variable, as well. And naturally, you're not limited to just one variable. You can pass in as many as you want, as long as they're separated by commas... static void PrintFullName(string firstName, string middleName, string lastName) { Console.WriteLine("Your name is {0} {1} {2}.", firstName, middleName, lastName); } When calling the method, each variable you pass is called a "parameter", or in some cases an "argument". As previously mentioned, you just separate each argument with a comma: string lastName = "Miller"; // you can pass in variables, as well PrintFullName("Frank", "Lloyd", lastName); The parameters don't all have to be the same datatype, by the way; they can be anything you want. Just remember that when you actually call the method, the quantity and datatype of the parameters you supply must match what you've specified in the method's signature. You could not, for example, call PrintFullName() and only provide one string, nor could you provide two strings and an integer (although you could call ToString() on the integer to convert it to a string, and then it would work just fine). You might be wondering if the names of the variables you pass in have to match the names of the variables in the method. The answer is, simply, no! The very fact that you can pass in a hard-coded string with no variable name at all should illustrate that the name doesn't matter. Personally, I like to use the same names both inside and outside of the method, just to clarify where the data is coming from and going to, but as always, you can name the variables whatever you want. As with methods, however, be as specific as possible when naming parameters. For example, "multiplier" is a terrible parameter name, because all it tells you is that it's probably used to multiply something in your method. Well heck, you could figure that out just by looking at the actual line of code it's used in. On the other hand, if you call it something like "gravityRatio" or "drawingScale", the purpose of the variable becomes much more apparent. By the way, what happens if you want to pass an array into a method? Well, don't let the [] things scare you; an array is just another datatype. What's more, the actual number of elements doesn't matter, as far as a method signature is confirmed! If you need to pass a string[], your method would look like this: [STAThread] static void Main(string[] args) { string[] meh = new string[10]; // 10, 20, whatever... the signature doesn't care! DoSomethingWithAnArray(meh); } static void DoSomethingWithAnArray(string[] myArray) { // Do something } Okay, so now we can pass information into our methods. Unfortunately, the communication is going to be very one-sided if the method has no way to pass information back to its caller. Notice how all the methods we've been creating have "void" in their sign? Well, we can change that word to a datatype, in order to make the method return information to its caller. Observe this simple example: [STAThread] static void Main(string[] args) { int additionResult = AddNumbers(5, 4); } static int AddNumbers(int num1, int num2) { return num1 + num2; } You pass two integers into the method, and it "returns" the result of adding them together. As always, the return value can be represented in many different ways: it can be a hard-coded integer, like 45, or an integer variable, or the result of integer math, as shown above. The important thing is, the value that you put after "return" must have an integer datatype. On the "calling" end, we use the = sign to store (or use) the results of the method call. What if you don't do anything with the return value, but simply call the method like you did with the void examples earlier? Well, nothing would happen. C# would just forget about the value that was returned by the method, and that would be that. It works exactly like Console.ReadLine(), in that you can store or use the value returned by the call, but you don't have to. Conversely, something you must do is provide a return statement in any method that has a return type other than void. Why? Because "return" is the method's cue to exit itself and return with a value. When you specify a return type other than "void", you are basically promising C# that the method will always return a value of that datatype. Incidentally, you can also use "return" to exit prematurely from a "void" method, except that you don't put a value after it (just the semicolon). An interesting thing about "return" is that it doesn't have to go at the end of the method. For example, you could theoretically do this: static int MultiplyNumbers(int num1, int num2) { return num1 * num2; Console.WriteLine("Hi, mom!"); } However, this would be kind of pointless, because the program will never hit the WriteLine() statement. "return" tells the program to break out of the method right now, and return to the calling method with the value you've given it. Sometimes, you might also see multiple return statements, like this: static string GetMultiplyResultSign(int num1, int num2) { int product = num1 * num2; if( product < 0 ) return "-"; else if( product > 0 ) return "+"; else return " "; } This is perfectly legal, although it's best to avoid multiple return statements when you can. For example, an alternative to the example shown above would be: static string GetMultiplyResultSign(int num1, int num2) { string answer = " "; int product = num1 * num2; if( product < 0 ) answer = "-"; else if( product > 0 ) answer = "+"; return answer; } It tends to be a little more readable, and it's also easier to predict how the method will behave. As an added bonus, you don't need an "else" case anymore, because "answer" already stores the default return value, right from the get-go. If you feel some strange, unexplainable urge to use multiple return statements, however, you're allowed to place them wherever you want, as long as it's impossible to reach the bottom of the method without hitting a "return". For example, putting the method's only return inside an "if" statement won't work, because it's possible that the code in the "if" will never run... you'd have to put another return in an else, or at the very end of the method to cover this possibility. The only situation where you don't need a "return" statement is when you're throwing an exception that will be unhandled. This is because an exception also causes a method to break immediately, just as a "return" would. Let's take a look at how this would work: static int DivideNumbers(int num1, int num2) { if( num2 != 0 ) return num1 / num2; else throw new Exception("Hey, stupid! No dividing by zero!"); } If the exception is hit instead of the return, the method will immediately break, and return to the call, just as a "return" would. What, then, would the return value be? The answer is that it doesn't matter, because it will never be evaluated. Remember what happens when an exception is thrown? Your program skips all the code in its path until it finds a "catch" statement that can handle the exception. Let's say that Main() calls CalculateNumbers(), which in turn calls DivideNumbers(). If an exception is thrown in DivideNumbers(), the program will skip the remaining code in DivideNumbers() until it finds a "catch". If there isn't a "catch" in DivideNumbers(), the program will return to the calling point in CalculateNumbers(), and continue skipping code until it finds a "catch". If it can't find a "catch" there, either, it returns to Main(), and continues skipping/searching. And of course, if it hasn't found a "catch" by the end of your program, it crashes. What this basically means is that you can either place a try/catch inside your method, or a try/catch around the line that calls the method, and either way it will handle any exception thrown by the method call. Where you actually place the try/catch is up to you, depending on whether you want the method to break back to its caller, or handle the exception within itself. As a rule of thumb, however, it is generally considered the original method's responsibility to provide valid information to a method, and deal with any errors that its call might cause. One way that we can immediately put all this knowledge to use is to write a method that gets an integer from the console for us... static int GetInt(string msg) { int returnValue = 0; bool isError = true; while( isError ) { try { Console.Write(msg); returnValue = Convert.ToInt32(Console.ReadLine()); isError = false; // value not set until successful read } catch{} } return returnValue; } With this method in place, we no longer need to worry about gracefully handling the possible errors that might occur from reading an integer from the console. This method will continually prompt the user for an integer, until the user finally enters valid input. All the calling method has to do is supply the prompt (ie, "Enter your age: "), and assign the result to a variable. And therein lies another benefit of methods: we have the beginnings of what programmers refer to as "encapsulation". "Encapsulation" simply means that we can hide most of the details that would otherwise clutter our view, and only expose the few details that we actually care about. When we call GetInt(), we don't have to think about try/catch, or "while" loops, or Console.ReadLine(), or any of that crap. We worried about all those little details just once, when we actually wrote the GetInt() method. Now, whenever we want an integer from the console, we just call GetInt(), and all we have to think about is the prompt to pass in, and how to use the result we get back! Encapsulation lets us program at a higher level: it allows us to stop thinking about our programs in terms of integers, and strings, and boolean logic, and start thinking about our program in terms of its actual function, whether that would be PrintSpreadsheet(), UploadFile(), DominatePlanet() or whatever. Encapsulation rocks. By the way, a method that returns a value is as good as an actual variable of that datatype. If you're only going to use the results of a method call once, in an "if" statement, you can actually save yourself the trouble of involving any variables at all, by doing this: if( GetInt("Enter your age: ") > 15 ) { Console.WriteLine("Old enough to drive!"); } We've talked quite extensively in this lesson, so it's probably time for a break. Don't think that we're done with methods, though - things are just starting to get interesting! using System; namespace A_MethodsScope { class Class1 { [STAThread] static void Main(string[] args) { // Call methods PrintLine(); string meaningOfLife = GetTheMeaningOfLife(); Console.WriteLine(meaningOfLife); PrintLine(); // Get results from methods int number1 = GetInt("Enter number 1: "); int number2 = GetInt("Enter number 2: "); int total = AddNumbers(number1, number2); Console.WriteLine(Environment.NewLine + "The total is " + total); // Use method results directly (without storing them in a variable) if( AddNumbers( number1, number2 ) % 2 == 0 ) Console.WriteLine("That's an even number."); else Console.WriteLine("That's an odd number."); // Prompt for exit Console.Write(Environment.NewLine + "Program complete! Hit enter to exit..."); Console.ReadLine(); } // Returns nothing, takes no parameters static void PrintLine() { Console.WriteLine("---------------------------------------------------"); } // Returns something, takes no parameters static string GetTheMeaningOfLife() { return "The meaning of life is equal to 7."; } // Returns something, takes a parameter static int GetInt(string msg) { int returnValue = 0; bool isError = true; while( isError ) { try { Console.Write(msg); returnValue = Convert.ToInt32(Console.ReadLine()); isError = false; // value not set until successful read } catch{} } return returnValue; } // Take more than one paramter, and return a result static int AddNumbers(int num1, int num2) { return num1 + num2; } } } |
|