25

I am creating an object model for a device that has multiple channels. The nouns used between the client and I are Channel and ChannelSet. ("Set" isn't semantically accurate, because it's ordered and a proper set isn't. But that's a problem for a different time.)

I'm using C#. Here is a usage example of ChannelSet:

// load a 5-channel ChannelSet
ChannelSet channels = ChannelSetFactory.FromFile("some_5_channel_set.json");

Console.Write(channels.Count);
// -> 5

foreach (Channel channel in channels) {
    Console.Write(channel.Average);
    Console.Write(",  ");
}
// -> 0.3,  0.3,  0.9,  0.1,  0.2

All is dandy. However, the clients are not programmers and they absolutely will be confused by zero indexing - the first channel is channel 1 to them. But, for the sake of consistency with C#, I want to keep the ChannelSet indexed from zero.

This is sure to cause some disconnects between my dev team and the clients when they interact. But worse, any inconsistency in how this is handled within the codebase is a potential problem. For example, here's a UI screen where the end user (who thinks in terms of 1 indexing) is editing channel 13:

Channel 13 or 12?

That Save button is eventually going to result in some code. If ChannelSet is 1 indexed:

channels.GetChannel(13).SomeProperty = newValue;  // notice: 13

or this if it's zero indexed:

channels.GetChannel(12).SomeProperty = newValue;  // notice: 12

I'm not really sure how to handle this. I feel like it's good practice to keep an ordered, integer-indexed list of things (the ChannelSet) consistent with all of the other array and list interfaces in the C# universe (by zero indexing ChannelSet). But then every piece of code between the UI and the backend will need a translation (subtract by 1), and we all know how insidious and common off-by-one errors already are.

So, has a decision like this ever bitten you? Should I zero index or one index?

  • 60
    "Should array indices start at 0 or 1? My compromise of 0.5 was rejected without, I thought, proper consideration." — Stan Kelly-Bootle – gnat Jul 10 '15 at 0:57
  • 2
    @gnat It's a good thing I'm alone in the office - bursts of laughter staring at the computer screen aren't usually good indicators of productivity! – kdbanman Jul 10 '15 at 1:04
  • 4
    Do channels have to be identified by their position in the set? Can't you identify them by something else instead (e.g. frequency if those were TV channels)? – svick Jul 10 '15 at 1:53
  • @svick, that's a great point. I may allow channel access by different identifiers later, but the primary lingo of the clients really seems to be one indexed channel numbers. – kdbanman Jul 10 '15 at 5:30
  • From a quick read it seems like there is no need to use indices as a matter of efficiency, so you could use some kind of Map to directly link channel IDs and channels without needing to think about arrays. I think this is what Rob is getting at, and I also agree with his solution if you do opt for using indices. – Matthew Read Jul 10 '15 at 18:22
24

It feels like you're conflating the Identifier for the Channel with its position within a ChannelSet. The following is my visualisation of how your code/comments would look at the moment :

public sealed class ChannelSet
{
    private Channel[] channels;
    /// <summary>Retrieves the specified channel</summary>
    /// <param name="channelId">The id of the channel to return</param>
    Channel GetChannel(int channelId)
    {
        return channels[channelId-1];
    }
}

It does feel like you've decided that because Channels within a ChannelSet are identified by numbers that have an upper and lower bound they must be indexes and therefore as it's C#, 0 based. If the natural way to refer to each of the channels is by a number between 1 and X, refer to them by a number between 1 and X. Don't try and force them into being indexes.

If you really want to provide a way to access them by 0 based index (what benefit does this give your end user, or developers that consume the code?) then implement an Indexer:

public sealed class ChannelSet
{
    private Channel[] channels;
    /// <summary>Retrieves the specified channel</summary>
    /// <param name="channelId">The id of the channel to return</param>
    public Channel GetChannel(int channelId)
    {
        return channels[channelId-1];
    }

    /// <summary>Return the channel at the specified index</summary>
    public Channel this[int index]
    {
        return channels[index];
    }
}
  • 2
    This is a really complete and concise answer. It's exactly the approach I ended up deriving from the other answers. – kdbanman Jul 10 '15 at 15:18
  • 7
    Passersby, I've accepted this answer, but this thread is full of good answers, so read onwards. – kdbanman Jul 10 '15 at 15:20
  • Very nice, @Rob. "I know how to use Indexers." - Neo – Jason P Sallinger Jul 13 '15 at 17:07
35

Show the UI with index 1, use index 0 in code.

That said, I worked with audio devices like this and used index 1 for the channels and designed the code never to use "Index" or indexers to help avoid frustration. Some programmers still complained, so we changed it. Then other programmers complained.

Just pick one and stick with it. There are bigger problems to solve in the grand scheme of getting software out the door.

  • 12
    The exception to this: if you're using a 1-based language. Your code should be consistent with the rest of your code and your language, so 0-based in C#, 1-based in VBA (!) or Lua. Your UI should be whatever humans expect (which is almost always 1-based). – immibis Jul 10 '15 at 6:02
  • 1
    @immibis - good point, I had not thought of that. – Telastyn Jul 10 '15 at 11:27
  • 3
    One thing I do occasionally miss about the olden days programming in BASIC is "OPTION BASE"... – Brian Knoblauch Jul 10 '15 at 13:22
  • 1
    @BlueRaja-DannyPflughoeft: While I always thought Option Base was silly, I did like the feature some languages offered of allowing arrays to individually specify arbitrary lower bounds. If one has an array of data by year, for example, beyond able to have an array dimensioned [firstYear..lastYear] can be nicer than having to always access element [thisYear-firstYear]. – supercat Jul 11 '15 at 14:46
  • 1
    @BlueRaja-DannyPflughoeft One man's bug is another man's feature... – Brian Knoblauch Jul 12 '15 at 11:41
21

Use both.

Do not mix the UI with your core code. Internally (as a library) you should code "without knowing" how each element on the array will be called by the final user. Use the "natural" 0-indexed arrays and collections.

The part of the program that joins the data with the UI, the view, should take care to correctly translate data between the User's Mental Model and the 'Library' that does the actual work.

Why is this better?

  • The code can be cleaner, no hacks to translate indexing. This also helps the programmers by not expecting them to follow and remember to use unnatural conventions.
  • The users will use the indexing that they expect. You don't want to annoy them.
12

You can see the collection from two different angles.

(1) It is, in the first place, a regular sequential collection, like an array or a list. Index from 0 is obviously the right solution then, following the convention. You allocate enough entries and map channel number to indices, which is trivial (just subtract 1).

(2) You collection is essentially a mapping between channel identifiers and channel info objects. It just happens that channel identifiers is a sequential range of integers; tomorrow it might be something like [1, 2, 3, 3a, 4, 4.1, 6, 8, 13]. You use this ordered set as mapping keys.

Choose one of the approaches, document it, and stick to it. From flexibility standpoint, I'd go with (2), because the representation of channel number (at least the displayed name) is relatively likely to change in the future.

  • I really appreciate part 2 of your answer. I think I'll adopt something like it. The index accessor (channels[int]) will be zero indexed integers, and the Get accessors GetChannelByFrequency, GetChannelByName, GetChannelByNumber will be flexible. – kdbanman Jul 10 '15 at 5:36
  • 1
    The second approach is definitely best. My local broadcasters offer 32 channels with LCNs ranging from 1 to 201. This would require a sparse array (84% empty). It is conceptually like storing a collection of people in an array indexed by telephone number. – Kelly Thomas Jul 10 '15 at 7:55
  • 3
    Furthermore, any collection so small that it's represented by a UI dropdown is extremely unlikely to benefit from the specific performance characteristics of an array as the data structure. So the thing called "channel 1" by the user might as well be called "1" in the code too, and use a Dictionary or SortedDictionary. – Steve Jessop Jul 10 '15 at 10:44
  • 1
    The principle of the least surprise would imply that operator [] (int index) should be 0-based, while operator [] (IOrdered index) would not. (Apologies for the approximate syntax, my C# is very rusty.) – 9000 Jul 10 '15 at 15:24
  • 2
    @kdbanman: personally, I would argue that as soon as you're overloading [] in a language that uses this overload for lookup by arbitrary key, then programmers must not assume keys start from 0 and are contiguous, and should kick that habit sharpish. They might start from 0, or 1, or 1000, or the string "Channel1", that's the whole point of using operator [] with arbitrary keys. OTOH, if this was C and someone was saying "should I leave element 0 in an array unused in order to start from 1" then I wouldn't say "obviously, yes", it'd be a close call but I'd lean to "no". – Steve Jessop Jul 11 '15 at 12:28
3

Everybody and their dog uses zero based indexes. Using one-based indexes inside your application will cause you maintenance problems forever.

Now what you display in a user interface, that is entirely up to you and your client. Just display i+1 as the channel number, together with channel #i, if that makes your client happy.

If you expose a class, then you expose it to programmers. People you are confused by a zero-based index are not programmers, so there is a disconnect here.

You seem to worry about the conversion between UI and code. If that worries you, create a class carrying the user interface representation of a channel number. One call turning the number 12 into the UI representation "Channel 13", and one call turning "Channel 13" into the number 12. You should do that anyway, in case the customer changes their mind, so there are just two lines of code to change. And it works if the customer asks for roman numerals, or letters A to Z.

  • 1
    I cannot verify your answer since my dog won't tell me what type of indexes he uses. – armani Jul 10 '15 at 21:17
3

You are mixing up two concepts: indexing and identity. They are not the same thing and should not be confused.

What is the purpose of indexing? Fast random access. If the performance isn't an issue, and given your description it isn't, then using a collection that has an index is unnecessary and probably accidental.

If you (or your team) is being confused by the index vs the identity, then you can solve that problem by not having an index. Use either a dictionary or an enumerable and get the channel by value.

channels.First(c=> c.Number == identity).SomeProperty = newValue;
channels[identity].SomeProperty = newValue;

The first is more clear, the second is shorter -- they may or may not get translated into the same IL, but either way, given you are using this for a dropdown, it should be fast enough.

2

You are exposing an interface through your ChannelSet class that you expect your users to interact with. I would make it as natural for them to use as possible. If you expect your users will start counting from 1 instead of 0, then expose this class and its usage with that expectation in mind.

  • 1
    Therein lies the problem! My end users are expecting to start from 1, and my dev users (myself included) are expecting to start from 0. – kdbanman Jul 10 '15 at 1:05
  • 1
    And so I suggest adapting your class to meet the needs of your end users then. – Bernard Jul 10 '15 at 1:06
2

0-based indexing was popular, and defended by important people, because the declaration shows the size of the object.

With modern computers, with the computers your users will be using, is the size of the object important at all?

If your langauge (C#) doesn't support a clean way of declaring the first and last element of an array, you can just declare a bigger array, and use declared constants (or dynamic variable) to identify to the logical start and end of the active area of the array.

For UI objects, you can almost certainly afford to use a dictionary object for your mapping, (if you've got 10 million objects, you've got a UI problem), and if the best dictionary object turns out to be a list with wasted space at either end, you haven't lost anything important.

  • Actually the zero-based and one-based indexes have to do with how the array is constructed in memory. Zero-based indexes use external memory (external to the array) to determine size, while one-based indexes use internal memory (index 0) to remember the size of the array (usually, anyways). It's not popular for "the size of the object" but usually has to do with how the memory is internally allocated for arrays. Regardless, I wouldn't recommend recklessly leaking bytes here and there "just because." But dictionaries are usually a better idea anyways. – phyrfox Jul 10 '15 at 22:01
  • @phyrfox: The abstraction I prefer for zero-based indexing is to say that indexes identify positions between objects; the first object lies between indices 0 and 1, the second between 1 and 2, etc. Given x<=y<=z, ranges [x,y] and [y,z] will be consecutive but not overlap, and either or both may be empty with no special corner cases required. When adopting the "indices sit between items" model with zero-based indexing, a list of six items spans the range from 0 to 6; with one-based indexing it would sit from 1 to 7. Note that this abstraction fits well with "one-beyond" pointers. – supercat Jul 11 '15 at 14:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.