12
\$\begingroup\$

I have only been self teaching myself programming on and off for about 5 months. The purpose of me writing this class is to incorporate the knowledge that I have gathered since I started.

using System;
using System.Collections;
using System.Linq;

namespace GenericDataStructures
{
   public sealed class ArrayList<T>: IArrayList<T>, IEnumerable
   {
    private const int DEFAULT_SIZE = 8;
    private T[] listArray;
    private int arrayElementCounter = 0;
    private T[] newArrayForCopying = null;

    public ArrayList()
    {
        listArray = new T[DEFAULT_SIZE];
    }

    public int Count{get{return this.arrayElementCounter;}}

    public void Add(T element)
    {
        growIfArrayIsFull();
        this.listArray[arrayElementCounter] = element;
        this.arrayElementCounter++;
    }

    public void Remove(T element)
    {
        int indexOfFoundElement = IndexOf(element);
        if (indexOfFoundElement != -1)
        {
            newArrayForCopying = new T[listArray.Length];
            // 3 cases to handle
            // Case 1. only one element in the list
            if (arrayElementCounter == 1)
            {
                IfElementToBeRemovedIsHeadAndTheOnlyElement();
                return;
            }
            // Case 2. Element To be removed is head but not the only element
            else if (Equals(listArray[0], element) && arrayElementCounter > 1)
            {
                IfElementToBeRemovedIsHeadButNotTheOnlyElement(newArrayForCopying);
                return;
            }
            // Case 3. Element to be removes is not the head and not the only one in the list
            else if(indexOfFoundElement > 0 && indexOfFoundElement < arrayElementCounter) // The element To be removed is not the tail
            {
                IfElementToBeRemoveIsNotHeadAndNotIsNotTail(newArrayForCopying, indexOfFoundElement);
                return;
            }
        }

        return;
    }

    public void RemoveAt(int index)
    {
        newArrayForCopying = new T[listArray.Length];
        if (index < 0 || index > listArray.Count())
        {
            throw new ArgumentOutOfRangeException();
        }
        if (index == 0 && arrayElementCounter == 1)
        {
            IfElementToBeRemovedIsHeadAndTheOnlyElement();
            return;
        }
        else if (index == 0 && arrayElementCounter > 1)
        {
            IfElementToBeRemovedIsHeadButNotTheOnlyElement(newArrayForCopying);
            return;
        }
        else if (index > 0 && index < arrayElementCounter)
        {
            IfElementToBeRemoveIsNotHeadAndNotIsNotTail(newArrayForCopying, index);
            return;
        }
        else
        {
            IfElementIsTail(newArrayForCopying);
            return;
        }

    }

    public void Clear()
    {
        listArray = new T[DEFAULT_SIZE];
    }

    public bool Contains(T element)
    {
        for (int i = 0; i <= arrayElementCounter; i++)
        {
            if (this.listArray.Equals(element))
            {
                return true;
            }
        }
        return false;
    }

    private void growIfArrayIsFull()
    {
        T[] newListArray;
        if (this.arrayElementCounter == this.listArray.Length)
        {
            newListArray = new T[this.listArray.Length * 2];
            Array.Copy(listArray, newListArray, arrayElementCounter);
            listArray = newListArray;
        }
        else
        {
            return;
        }
    }

    public IEnumerator GetEnumerator()
    {
        // This is not the most elegant solution. 
        // Will Change it when I understand more about 
        // what the this method is supposed to return
        return listArray.Take(arrayElementCounter).GetEnumerator();
    }

    public int IndexOf(T element)
    {
        // returns index of first instance of an element if present in the list
        // else returns an index of -1
        int index = 0;
        while (index != arrayElementCounter)
        {
            if (Equals(element, listArray[index]))
            {
                return index;
            }
            index++;
        }
        return -1;
    }

    private void IfElementToBeRemovedIsHeadAndTheOnlyElement()
    {
        listArray = new T[DEFAULT_SIZE];
        arrayElementCounter--;
        return;
    }

    private void IfElementToBeRemovedIsHeadButNotTheOnlyElement(T[] arrayForCopying)
    {
        Array.Copy(listArray, 1, arrayForCopying, 0, this.Count -1);
        arrayElementCounter--;
        listArray = arrayForCopying;
        return;
    }

    private void IfElementToBeRemoveIsNotHeadAndNotIsNotTail(T[] arrayForCopying, int indexOfElementToRemove)
    {
        Array.Copy(listArray, 0, arrayForCopying, 0, indexOfElementToRemove + 1);
        Array.Copy(listArray, indexOfElementToRemove + 1, arrayForCopying, indexOfElementToRemove, arrayElementCounter - indexOfElementToRemove);
        listArray = arrayForCopying;
        arrayElementCounter--;
    }

    private void IfElementIsTail(T[] arrayForCopying)
    {
        Array.Copy(listArray, 0, arrayForCopying, 0, arrayElementCounter -1);
        listArray = arrayForCopying;
        arrayElementCounter--;
        return;
    }
  }
}
\$\endgroup\$

2 Answers 2

8
\$\begingroup\$

Naming

Unlike in Java, constants in C# aren't normally named with all caps and underscores (DEFAULT_SIZE).

Methods names shouldn't be lower-case (growIfArrayIsFull).

Names such as IfElementToBeRemovedIsHeadButNotTheOnlyElement are way too long, and they don't describe the method well. A method should be named for what it does, not when or why it does that. A method doesn't know who calls it and in what scenario or context it happens. That's not its responsibility. A method knows what it does itself. The calling code knows when and why to use it. That's how responsibility is dealt. A pickaxe doesn't need to know it's in a coal mine.

When a method's name starts with "if", it's already very suspicious.

Structure

newArrayForCopying is a class field, and yet methods are passing it to eachother as a parameter. This is confusing: every method has access to all class members by itself, they don't need to piggyback on other methods for that purpose.

Redundant clauses

private void growIfArrayIsFull()
{
    T[] newListArray;
    if (this.arrayElementCounter == this.listArray.Length)
    {
        newListArray = new T[this.listArray.Length * 2];
        Array.Copy(listArray, newListArray, arrayElementCounter);
        listArray = newListArray;
    }
    else
    {
        return;
    }
}

the else-return bloc doesn't serve any purpose.

The same with return calls at the end of methods (Remove, growIfArrayIsFull, IfElementToBeRemovedIsHeadAndTheOnlyElement, IfElementToBeRemovedIsHeadButNotTheOnlyElement, IfElementIsTail): redundant. These methods are exited anyway.

You don't need to use both return and else clauses. It's one too many. Eg.:

if (index == 0 && arrayElementCounter == 1)
{
    IfElementToBeRemovedIsHeadAndTheOnlyElement();
    return;
}
else if (index == 0 && arrayElementCounter > 1)
{
    IfElementToBeRemovedIsHeadButNotTheOnlyElement(newArrayForCopying);
    return;
}

would make more sense as:

if (index == 0 && arrayElementCounter == 1)
{
    IfElementToBeRemovedIsHeadAndTheOnlyElement();
}
else if (index == 0 && arrayElementCounter > 1)
// ...

or:

if (index == 0 && arrayElementCounter == 1)
{
    IfElementToBeRemovedIsHeadAndTheOnlyElement();
    return;
}
if (index == 0 && arrayElementCounter > 1)
// ...

for reasons that I hope you can see.

listArray is always initialized to the same value (new T[DEFAULT_SIZE]). You could use a field initializer instead, and get rid of the parameterless constructor. Same effect, less lines of code.

When there is no ambiguity (like a parameter named the same as a field), and there isn't any in this code, you don't need to refer to fields by this., as in this.arrayElementCounter == this.listArray.Length. Especially since you're not consistent about it and this code sometimes does it, and sometimes not. Consistency is a highly valuable trait when it comes to code.

Other concerns

  • No documentation. It would be great if you documented the class with comments, especially as it's intended to be a general-use mechanism.

  • Thread safety. It doesn't provide it. Eg. if two different threads call Add at the same time, it's going to mess the state up. If thread safety isn't among your requirements, that's fine, but it's good to be explicit about it. I would put that information in documentation comments.

  • Nullability. How do we want this code to handle it? Did you consider that null elements can be added, that calling code can try to remove a null element or ask what its index is, that searching for a null element can end up with a NullReferenceException when Equals is called on a null, or a null is passed to it? Food for thought.

  • Remove is an operation that can fail if the element you wanted to remove wasn't present in the collection. It is customary - not only in C# - to return a value indicating whether the removal was successful or not. See https://msdn.microsoft.com/en-us/library/cd666k3e(v=vs.110).aspx

  • Type safety. Why does this class support non-generic version of IEnumerable but not the generic one (IEnumerable<T>)? The latter provides type safety. Type safety is better than the lack of it.

  • Unit tests. If you haven't, write unit tests for it (and submit them for code review as well!). It forces you to think better about the design, and catches bugs, like the one caught out by @RUser4512 in his answer.

Having said all that, I have to say this code is quite decent and promising for someone who have only been programming for a few months. There are slip-ups, but it shows you have good capability in structuring abstract concepts, and that's the core skill in programming.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ Thanks for an honest review of the code. I'll get back cleaning up the code. I'm not entries sure about thread safety. I haven't heard of the term before. Something new for me to learn. :) \$\endgroup\$
    – Nate
    Commented Oct 17, 2016 at 3:09
  • 1
    \$\begingroup\$ I don't blame you, it's an advanced concept, but one worth knowing, as there's no escape from it later on. Basically the problem occurs when operations are executed from more than one thread, eg. the Add method gets called by one thread, then by another one while the first one is still in the middle of this method! And arrayElementCounter falls out of sync with listArray, and weird things happen. For a start, look up the lock keyword. Thread-safety isn't always obligatory, it depends on the use case, but not providing it as a deliberate choice is not the same as being oblivious of it \$\endgroup\$ Commented Oct 17, 2016 at 8:13
  • \$\begingroup\$ Hey there! so sorry. Im new to all this. But thanks for reminding me! You're a kind soul. Your insight has been useful! \$\endgroup\$
    – Nate
    Commented Oct 18, 2016 at 12:57
  • \$\begingroup\$ Sure thing! Happy to be of help @Nate. Same with your other question, about a linked list. Marking an answer as accepted sends an important message to other beginners that you actually found the answer useful for your purposes (since everyone can cast votes, but the author of the question is the ultimate judge of whether they were helped out), so for educational... nah I'm just greedy for reputation points tbh. All the best with your improvement \$\endgroup\$ Commented Oct 20, 2016 at 8:30
6
\$\begingroup\$

Naming and conventions

You will need to impose some guidelines when it comes to naming. Per example, private and public methods are mixed, which makes it hard to read for someone willing to use your class. See per example : this stack overflow thread

Identation

If you are using (recent) versions of Visual Studio, Ctrl+k+d will automatic ident all the code for you, take care of missing spaces...

Consistency

You write :

  public void Clear()
    {
        listArray = new T[DEFAULT_SIZE];
    }

But you also use the keyword this sometimes :

public int Count{get{return this.arrayElementCounter;}}

Inside a class, you do not need to use this to access the variables of the class. Instead, a good way to keep track of the (private) fields is to name them with an underscore. Therefore, in a glance, you know what belongs and does not belong to the class.

Some bugs

I doubt this function works : you do not use the variable i and do not iterate over the elements of the vector to assess equality.

public bool Contains(T element)
{
    for (int i = 0; i <= arrayElementCounter; i++)
    {
        if (this.listArray.Equals(element))
        {
            return true;
        }
    }
    return false;
}

Memory management

I note that you use a private const int DEFAULT_SIZE = 8;. But a lot of C# libraries let the user define the default size when the container is constructed. I would encourage you to follow the same rule (it allow easy performance gains when you have an idea of the size of the container you will be working with).

\$\endgroup\$
6
  • \$\begingroup\$ “I doubt this function works”... you should explain why. \$\endgroup\$ Commented Oct 16, 2016 at 19:02
  • \$\begingroup\$ Doubling the size of the container is standard, both in text books and in practice: stackoverflow.com/a/5232342/5843932 \$\endgroup\$ Commented Oct 16, 2016 at 19:58
  • \$\begingroup\$ @ArturoTorresSánchez done thx for the feedback :) \$\endgroup\$
    – RUser4512
    Commented Oct 16, 2016 at 20:01
  • 1
    \$\begingroup\$ @JoshuaTaylor done, thanks for the feedback :) \$\endgroup\$
    – RUser4512
    Commented Oct 16, 2016 at 20:02
  • \$\begingroup\$ How does one allow the user to implement the container size at default? Since this is a list data structure, shouldn't hidden information as such be hidden from the user? \$\endgroup\$
    – Nate
    Commented Oct 17, 2016 at 3:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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