4 ms·
So I'm taking a look at this line: if(!memo.TryGetValue(rdr.GetInt32("student_id"), var out complete) memo.Add(new Student(rdr out complete).ID, complete
by 16bytes 10y ago
So I'm taking a look at this line:
if(!memo.TryGetValue(rdr.GetInt32("student_id"), var out complete)
memo.Add(new Student(rdr out complete).ID, complete);
I think I understand your point, but I found this code really hard to read. You don't use the first var out complete in the TryGetValue, right? And the Student c'tor returns itself as an out parameter?
If I understand you correctly, you like this because you don't have to declare a new student before you add it? I.e. the alternative would be
if(!memo.ContainsKey(rdr.GetInt32("student_id")))
var student = new Student(rdr);
memo.Add(student.ID, student);
I guess I would prefer this over the former. Also, why not enforce your student ID constraint in SQL instead of putting everything in a dictionary only to take it back out again? That would simplify your code to the point of just being a map from rdr->Student. Furthermore, if all I had was the Student c'tor, I would never guess that was the intent of the out parameter. This seems more anti-pattern than pattern.
I would rather put a simple IEnumerable in front of SqlDataReader so that you could just do:
foreach(var row in rdr) yield return new Student(row)
This doesn't obviate your use case, however, which is to inline a variable where it's needed in multiple places in that line because you can save yourself an explicit declaration. In this case, however, I think that the increased readability justifies the explicit declaration.
- noblethrasher 10y agoHi, thanks for taking the time to comment. > You don't use the first var out complete in the TryGetValue, right? There is only one complete variable; we declare it in TryGetValue, and it's definitely used in the last line of the while statement, but might first be used after the if statement. > And the Student c'tor returns itself as an out parameter? The Student constructor does not return itself as an out parameter, rather it returns an object that can modify an internal list of courses. The idea is that when we iterate through a result set from a database, some of the rows are going to correspond to a new student object, and some are going to correspond to a course that belongs to the student. Crucially, we are only allowed modify the Student object (or whatever) during iteration, and the collection that we return will only contain immutable/unmodifiable Student objects. I've used this technique to populate deeply nested structures (lots of joins and nested joins) using only one query. > I would rather put a simple IEnumerable in front of SqlDataReader so that you could just do: > foreach(var row in rdr) yield return new Student(row) Just to be clear, the reason that I cannot do that is because the Student object might not be completely "hydrated" until we finish iterating through the result set, because it might contain a bunch of nested objects that also need to be instantiated from one or more DB records. > Also, why not enforce your student ID constraint in SQL instead of putting everything in a dictionary only to take it back out again? I'm not quite sure I understand this (which is probably my fault), but rest assured that we use nothing but SQL (specifically, DDL) to enforce data integrity.
- 16bytes 10y agoThank you, in turn, for replying. I'm of the opinion that out variables have little value if you have tuples and deconstruction, but I wanted to understand your point. I'm not sure I do, unfortunately. I'm little out of practice with C#, so bear with me. Back to this code: if(!memo.TryGetValue(rdr.GetInt32("student_id"), var out complete) memo.Add(new Student(rdr out complete).ID, complete); There's a parens missing on the end of the if, correct? Also I can't parse the student c'tor: new Student(rdr out complete) I was assuming there is a comma in there somewhere. Does this compile? What does "out" do here? I thought you were getting a new out variable, but that's not correct since you wouldn't be able to name it the same in the same scope. Also, I don't see how "complete" is ever non-null. If the ID isn't in the dictionary, then TryGetValue returns false and "complete" is null. Then you add the null "complete" to the dictionary, and throw away the Student object (which apparently does other side effects) once you have its id? If ID is in the dictionary, you get back what you inserted, which is still null. And then you call .AddCourse on the possibly null reference? I'm lost. Can you post the code again? Maybe I'm just missing something due to a syntax error.
- noblethrasher 10y agoYou're right, there were missing tokens; sorry about that. Here is the equivalent C# 6 version of the code (i.e. something very similar to the pattern that I currently use): IEnumerable<Student> GetStudents(SqlCommand cmd) { var rdr = cmd.ExecuteReader(); var memo = new Dictionary<int, Student.Completion>(); while(rdr.Read()) { Student.Completion completion; //this declaration will be unnecessary in C# 7 if(!memo.TryGetValue(rdr.GetInt32("student_id"), out completion)) memo.Add(new Student(rdr, out completion).ID, completion); completion.AddCourse(rdr); //completion is *guaranteed* to be non-null } return from kv in memo select kv.Value.Student; } As you can see, it only differs from the C# 7 version by one line. The first thing to note is that, per the C# spec, the `out` parameters of a method must be definitely assigned before the method returns[1]. It just so happens that the constructor of the `Student` class always creates a new `Completion` object and assigns it to the `out` parameter. Now, theoretically, an `out` parameter could be assigned a null reference (as in the case of TryGetValue), but in practice it's trivial to guarantee that it will be non-null (as in the case of our Student cstor). In the line, memo.Add(new Student(rdr, out completion).ID, completion); we first call the Student cstor, which assigns a non-null value to completion, so that by the time `memo.Add` is called, the completion variable is guaranteed to be non-null. Also, `Student.Completion` is a class that is defined inside of the `Student` class. As such, it has access to all of Student's members (private and public). But, in order to do anything to an instance of Student, a Completion instance must have field which references that Student instance (unlike the case of Java's inner classes, which are a bit more powerful I think). That is why this line is possible: return from kv in memo select kv.Value.Student //`Value` is an instance of Student.Completion Here is the basic definition of the Student class: sealed class Student { public int ID { get; } //this is a readonly property, meaning it can only be modified in a cstor public FullName { get; } //readonly property private List<Course> courses = new List<Courses>(); public IEnumerable<Course> Courses => from c in courses select c; public Student(IDataReader rdr, out Completion completion) { ID = rdr.GetInt32("student_id"); Fullname = rdr.GetString("fullname"); completion = new Completion(this); } public sealed class Completion { public Student Student { get; } //readonly property. public Completion(Student student) { this.Student = student; } public void AddCourse(IDataReader rdr) { if(rdr.GetInt32("student_id) == Student.ID) student.courses.Add(new Course(rdr)); } } } Like I said in my earlier comment, my immediate goal was to be able to create a set of immutable objects with arbitrary nestings from the result of a single SQL query that may have an arbitrary number of joins (which is how we represent nesting relationally). The above example just has just one nested property, but I have production code in which objects have many more nested properties. For example, the `Course` class in the aforementioned example might have its own `Completion` class for adding `CourseAssignment` instances (e.g. select from Student left join Course on ... left join CourseAssignment on ...). But, the really big idea is that I wanted an object-capability system[2][3]. Getting objects be immutable "almost everywhere"[4] is a nice side benefit. [1] http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf http://www.ecma-international.org/publications/files/ECMA-ST... (section 12.1.6) [2] https://en.wikipedia.org/wiki/Object-capability_model https://en.wikipedia.org/wiki/Object-capability_model [3] https://www.youtube.com/watch?v=EGX2I31OhBE https://www.youtube.com/watch?v=EGX2I31OhBE [4] https://en.wikipedia.org/wiki/Almost_everywhere https://en.wikipedia.org/wiki/Almost_everywhere (in this case "almost everywhere" is with respect to the set of all possible execution paths).
- deleted 10y ago[deleted]