I've been supplementing my school course work with some Udemy courses. I'm trying to understand one example right now for intermediate C# and I'm a little confused about one of the details. I understand inheritance, but I'm a little confused about composition (specifically passing objects to objects and where they are initialized). In this example there is a DbMigrator class, Installer Class, and a Logger class that DbMigrator and Installer both borrow from. Here is Main:
namespace Composition
{
class Program
{
static void Main(string[] args)
{
var dbMigrator = new DbMigrator(new Logger());
var logger = new Logger();
var installer = new Installer(logger);
dbMigrator.Migrate();
installer.Install();
}
}
}
I understand the part where you create a logger object that is passed to the installer object, but I don't get the "new Logger()" constructor for dbMigrator object because there is no variable name for the logger that is being created. Wouldn't you need to create "var logger1 = new Logger()" inside the dbMigrator object's constructor? I don't understand how you can just pass "new logger()" without a variable name.
Here is the DbMigrator class:
namespace Composition
{
public class DbMigrator
{
private readonly Logger _logger;
public DbMigrator(Logger logger)
{
_logger = logger;
}
public void Migrate()
{
_logger.log = ("Blah blah blah");
}
}
}
I realize that the DbMigrator class has one initialized, but I would think an object needs to be created first.
Can someone clear this up for me?
So firstly, a summary of the question - how come this works?
var dbMigrator = new DbMigrator(new Logger());
Shouldn't it be this?
var logger = new Logger();
var dbMigrator = new DbMigrator(logger);
A fully detailed answer is quite long, so I'll try my best to make it nice and easy to follow. We'll do that by first describing what an expression is and how they work in C# (more broadly, how they work in any stack based language - fortunately that's almost all of the ones you've heard of!)
An operation: Something like add, subtract etc.
An expression: A bunch of operations. 1+2+3
or just a constant like 1
or "hi!"
A statement: Usually one expression followed by a ;
but something like an if
or a while
loop are also statements. If a program was a cooking recipe, a statement is simply one of the steps.
Let's take a quick look at that in C#:
int totalPrice=1+2*3;
That's one statement and it's broken down into operations like this:
totalPrice
A great question to ask at this point is then this:
So right after Multiply 2 by 3
runs, where is the 6 stored?
Answer: The stack. More on that shortly!
Method calls like Hello("all!");
accept expressions as arguments. Hello(1+1;);
fails, because that's a statement in there. Hello(1+(2*3));
is fine though.
Each operation takes in any number of operands then optionally outputs something. For example, the multiply operation takes in 2 operands - A * B - then outputs whatever they were multiplied together. new Something()
takes in any number of constructor arguments and outputs a reference to the newly created object.
Here's the important part.
The output of an operation always goes onto the stack. The inputs are always put onto the stack first too.
A full stack machine is out of scope here - if you're interested in the full details then check out e.g. this wikipedia article. The quick summary is all C based languages use this same concept. Let's revisit that C# statement above and describe it in stack operations:
int totalPrice=1+2*3;
Push 2 onto the stack
The stack is now just [2]
Push 3 onto the stack
The stack is now [2,3]
Multiply (Pops off two values from the stack and multiplies them)
The stack is now [6]
Push 1 onto the stack
The stack is now [6,1]
Add (Pops off two values from the stack, adds them together)
The stack is now [7]
Store in local totalPrice
(pops off the stack value and stores it)
The stack is now empty
Side note: These stack operations are what the C# compiler generates. It's called CIL or .NET IL.
Health warning! The call stack is something totally different. It's the call stack that can "overflow".
Alright so hopefully we've covered enough ground for this part to make more sense! So let's take the original statement:
var dbMigrator = new DbMigrator(new Logger());
Firstly, the new
operation is just like any other method call - all the arguments are pushed onto the stack, the method is invoked (popping them all off the stack) and the return value is then put on the stack.
So, here it is described stack style:
New Logger Object (Creates a Logger object on the heap and puts a reference to it on the stack. No args so it doesn't pop anything)
Stack is now [a logger reference]
New DBMigrator Object (Pops the 1 arg off, pushes the reference)
Stack is now [a DBMigrator reference]
Store in local dbMigrator
Stack is now empty
To really cement that in, compare it to the alternative (which is also valid):
var logger = new Logger();
var dbMigrator = new DbMigrator(logger);
New Logger Object
Stack is now [a logger reference]
Store in local logger
Stack is now empty
Load local logger
Stack is now [a logger reference]
New DbMigrator Object
Stack is now [a DBMigrator reference]
Store in local dbMigrator
Stack is now empty
What you also just learned: They both work, but the second one is slightly slower - it does more. Looking at those stack operations, it appears to do something completely pointless - It's like you just put something in a filing cabinet, then immediately take it out again!
As a quick side note, there are languages that don't have locals and only use the stack. They can be hugely optimized and tend to go faster, but they're also harder to write. Locals are great when you need multiple references to something:
Logger logger=new Logger();
// Using it twice!
logger.A();
logger.B();
If you were only using it once, then this is perfectly valid too:
(new Logger()).A();
Bonus round: Let's say that method A
did return this;
resulting in a reference to that Logger object on the stack again. That'll let us do this:
(new Logger()).A().B();
This is method chaining - it's related to functional programming and is getting increasingly more common because of how compact it is.
Local variables aren't the only way of storing things - the stack does it too! You don't need to keep track of the stack - the compiler does all of that for you. You just need think about those input/ output values.