The problem you're asking about is a non-problem, but the structure you've come up with is very inefficient.
Consider a simple 128x128x128 chunk. That will require 2097152 instances of
Block. You want
Block to be as small as possible. One thing that is easy to get rid of is the position. The position is implicit in the array containing the
Block instance. Nobody can retrieve the block without knowing its position. Hence, the position field of
Block is redundant and can be removed.
Another thing you want is to keep the structure fast. That's a lot of blocks per chunk you'll be working with for rendering, physics, gameplay, etc. One of the simplest things to keep in mind when working with performance is the CPU cache and memory accesses. Accessing random locations in memory is not free; they usually incur a pipeline stall in the CPU that can be quite a heavy hit on modern CPUs. The easiest way to combat this is to keep all your data contiguous in memory. Thankfully, this is what a
List or arrays do already. However, if you have a
List or array of classes, the data structure is technically only keeping references to the objects in its contiguous memory. The actual objects themselves could be strewn all over the place. C# was well-designed and includes the idea of a struct as an alternative to a class (unlike Java; Minecraft has to jump through more hoops to keep things contiguous in memory). Converting
Block to a struct will make accessing them faster. However, structs will actually cause the inheritance problem you were originally worried about, since a
Block is a struct) actually wouldn't be able to hold subclasses.
The final trick then is to simply not use classes and inheritance where they aren't needed, and this is one such place. In general, inheritance is a very, very poor tool for such problems. Use inheritance when you want to model is-a relationships, keeping in mind how a computer works/things - the computer has no reason to care that a stone block is a type of block. That's a human taxonomy problem. The computer only cares whether a stone block exposes the behavior of a block. That is, inheritance to a computer is mostly valuable only when you're modeling interfaces and behaviors.
Furthermore, inheritance starts to cause some serious issues. Say you have an ice block that has sliding behavior and a puzzle block that can be pushed and then want to add an ice puzzle block. Does it inherit from
IceBlock? Do you move all the common functionality up into
Block and make every single block pay the price for the excess bloat? The concept of aggregation solves this nicely. You can make an
IceBlockComponent and a
PuzzleBlockComponent and now an individual
BlockType can contain a list of these components, allowing you to mix-and-match behaviors however you want.
Alternatively, if you don't feel you need the full flexibility (and resulting complexity) of component-based design, move your inheritance structure out of
Block and into a
BlockType hierarchy. So you'd have a
BlockTypeIce and still only a single
Going back to the data-structure problem, not that each
Block does not need to contain all the behavior and methods of its type. In such a system, it needs only point back to its
BlockType. This can be done efficiently by storing all the
BlockTypes in an array or
List and then storing a small integer (say a
byte or a
short) in the
Block object. Now you no longer have any need for inheritance in
Block and you've reduced the size from before (no need for 4-8 byte references in your
Chunk array of blocks but rather a compact struct containing a small integer... or just the integer itself with no struct!).
The remaining hassle then is data. Thankfully, most data in these games are going to be per type rather than per block instance. That is, your
IceBlockComponent may have a
slipperiness attribute which would apply to all blocks of that type. There's no need to store the value on each individual block.
For the few bits of data you do need per block, just use a few bits (or a couple bytes). If your
Block is just an 8-bit index to a
BlockType, you could toss in another 8 or 16 bits and still have
Block stay relatively small and compact. These extra bits of data would be interpreted as needed by the block type (managing the meaning of the bits in a component/aggregation model is a little tricky, but doable).
The end result of all these is that you've got a very compact
Chunk that uses much less memory and is much faster to process than what you had before. You've also simplified your use of the type system and (if you play your cards right) made the game easier to mod and extend in the future.