A random collection of unique integers is a requirement for most card games and for many other applications as well. This is the last article in a four part series about different ways to create a random selection of unique integers. The earlier articles cover common but flawed methods recommended in other pages and show how those problems can be corrected. This final article is a comprehensive comparison of C# and VB in solving the problem. (You can start at the beginning using this link.)
"Chris", who provided most of the content of part 3, takes it to another level in this article and converts the whole thing to C# to see what differences and advantages that offers. This article, then, isn't really about creating unique arrays of integers anymore ... it's about C# and VB language features using unique arrays as a specific focus. I did a fair amount of code polishing before publishing the third article but in this one, I decided to let Chris's code speak for itself. Code reading is an art and figuring out how the code here actually works is tutorial all by itself.
Chris wrote ..."Remembering that we're talking about shuffling cards, we'll then want to think about dealing some or all of them. So we'll be consuming the returned deck as an Enumerable. Therefore, why not take advantage of that fact by returning an Enumerable? VB still doesn't have good support for iterator blocks. (Although in researching whether they were there yet, I see the plan for VB's iterators would be more powerful than C#'s yield as it would allow anonymous iterators declared in a function. But I digress...) So, I converted the shuffling classes to C# and, where possible, took advantage of the iterator block to return values as they were ready."
Since this is a Visual Basic site, we won't go into any detail about iterator blocks. But just so you can follow along with Chris, In C#, an iterator performs a custom iteration over an array or collection using the yield keyword. The yield return statement causes an element in the source sequence to be returned immediately to the caller before the next element in the source sequence is accessed. Here's the classic C# code that I think even programmers who have never coded C# can probably follow:
static IEnumerator GetCounter()
{
for (int count = 0; count < 10; count++)
{
yield return count;
}
}
Chris's C# code is considerably more complex, but it follows the same logic. The key statement in one is:
yield return deck[replaceLocation];
Think of it as a preview of things to come. I'm sure Chris is right that you'll see something like this in the next version of VB. But, just as I demonstrated that lambda expressions didn't really add that much in previous parts of this series, iterator blocks have a down side too. A typical iterator block will be compiled into a considerable pile of code to do it's magic. As Chris notes when he started getting timing results, "it had actually slowed down in the C# version due to the overhead of the code that the C# compiler uses to implement the iterator block."
Getting back to VB, one of the new things in Chris's code is the ability to deal just some of the cards. That is, return just some of the unique random integers from a larger array. New in this version of his code are:
- SelectAllFromDeck.vb
- SelectNFromDeck.vb
- SelectTenFromDeck.vb
The way he accomplishes this is the use of the Take method in the Enumerable object. ("John" also uses this method in the second article.) The key statement here is:
shuffler.GetShuffledDeck(deckSize).Take(handSize).ToList()
Earlier, Chris wrote that he would "take advantage ... by returning an Enumerable". This is what he was talking about. Take is one of literally dozens of methods available in the Enumerable class to do some very neat tricks like this. Want to disregard some of the items in a collection based on some condition? Use SkipWhile. Min(Of TSource)(Func(Of TSource, Int32)) performs a transform on each element and then returns the minimum value. Why would you ever want to do this? I don't know, but if you ever do, the method is there.
As sometimes happens when you start writing code just to see what happens, the end result is just what you expect. As Chris puts it, "As you might be able to work out from thinking about it, the Duplicate Check has a curve, as the risk of a collision at each position increases substantially, so by the final draw, we are having a considerable number of retries. (My back-of-the-napkin analysis puts it at 69 retries until you have a > 0.5 chance of having found the remaining card when dealing with a 100 card deck.) It does very well against Array Shuffle and Fisher Yates until you start drawing more than about 20% of the deck.. The "Lambda" (aka Random Sort) method is nearly flat, as you could probably guess as well. There is a slight increase which can be put down to the additional iteration we are doing to retrieve the cards. Fisher Yates performs better than Array Shuffle until you get to drawing about 70-75% of the deck."
Other than to wonder how big Chris's napkin had to be to calculate that probability, there are no surprises here. Chris even attached the graph he produced to show the results.
--------
Click Here to display the illustration
--------
Anyway, there you have it. I'll bet "Sam", who sent the original email, never expected this. I know I didn't. I encourage you to work on reading Chris's code. I'll bet there are few programmers out there who wouldn't learn something from it.
