ℙrinciples of ℂomputer ℙrogramming

2024-11-19

Credits

Purpose

This website contains all the resources to learn the principles of computer programming using C#. It is used in the delivery of CSCI 1301 - Principles of Computer Programming I and CSCI 1302 - Principles of Computer Programming II in the Bachelor of Science in Computer Science at Augusta University, and contains practical guides and additional resources for students and instructors.

Authors

At the time of writing, this resource is actively maintained Clément Aubert. Additional contributions, by (under)graduate course assistants and other contributors, are tracked by version control.

Some of the material originated from discussion, handouts and contributions by Clément Aubert, Aubrey Bryant, Michael Dowell, Richard DeFrancisco, Onyeka Ezenwoye, Leszek Gasieniec, Reza Rahaeimehr, Neea Rusch, Edward Tremel and Paul York.

Additionally, the School of Computer & Cyber Sciences’s past and present academic advisors, Laura Austin, Denise Coleman, Markus Bacha, and Wennie Squires, and communications & marketing specialist, Haley Bourne, improved the Academic Life notes through their suggestions and references.

Supports

The first source of support is the constant stream of feedback we receive from students and users: thank you.

This project has been monetarily supported by an Affordable Learning Georgia Transformation Grants (Proposal 571) and a Continuous Improvement Grant (M260).

“Affordable Learning Georgia”

This project also received the support of Augusta University’s School of Computer and Cyber Sciences and Center for Instructional Innovation.

Tools

We strive to prioritize open-source software when possible, and occasionally contribute to them.

Software

This website uses different technologies.

More details on the tools we use and how this resource is made can be found in dev. guide.

Fonts

We use the URW Gothic and Hack (inspired by the DejaVu font) fonts. Those fonts have been specially selected for their legibility and lower impact on environment.

Services

The source code and the website are graciously hosted and built by github.

Licence

This work is under Creative Commons Attribution 4.0 International. Concretely, this means that you are free to:

as long as you give proper credit and keep the same licence.

Please refer to our licence file for the detail of this licence.

Contributing

How can I contribute?

If you are a student

We would like to hear your thoughts on this resource to understand how to make it better for you and your fellow students. If you encounter a mistake, run into an issue while using the resource, or find it missing something important, you can contribute by providing feedback in one of the following ways:

If you have suggestions on how to make it better, we encourage you to share those ideas too.

If you are an instructor

You will need to have a Github account. Next contact any of the authors of this resource over email, provide your Github username, and request an invitation to be added to the instructors team.

If you are a UCA

You will need to have a Github account. Next ask your course section instructor to invite you to the 1301 UCAs team. Your instructor needs your Github username to send you the invitation.

If you are an outside collaborator

When you have identified a mistake in this resource and want to notify the authors, leave feedback on this website on the page where you notice the issue or open an issue explaining the issue.

If you want to make edits yourself, you can fork the source code, make edits, then open a pull request for us to review.

Next steps for editors

If you are looking to edit this resource and making your first contribution, read through the dev. guide. It explains:

Following the dev. guide will help to ensure your edits meet the expected quality guidelines and can be integrated into the existing resource with ease.

Dev. Guide

This guide explains how this resource is organized, how it is built and deployed, and how to maintain this resource. It is intended to be comprehensive, but should most likely be read only after having read our contributing and UCA guides.

Resources Organization Overview

Folders and Files

The source code repository’s main branch is organized as follows:

path description
.github/ github templates and configuration for github actions
misc/ resources that need to be either integrated into the resource, or discarded
source/ source for the material
licence.md license file
readme.md presentation of the repository

The source/ folder contains the following:

path description
code/ code examples (snippets and projects)
docs/ additional helpful documentation
solutions/ exercises (with solution)
fonts/ the fonts (redistributed with permission) used by this resource
img/ images, sometimes with their LaTeX source code
labs/ lab exercises
lectures/ lecture notes
projects/ projects (homework)
slides/ slides
templates/ templates and filters used for building this resource
uml/ UML diagrams
vid/ video files
.mermaid-config.json Mermaid configuration file
Makefile makefile used to compile this resource
index.md website index page
order file used to specify the order onthe website’s menu and the book
tags list of tags

Building and Deploying

The content is built and deployed in two phases:

Tools, Briefly

This resource is mainly developed and powered using

But note that knowing git and markdown are enough to contribute on-line through the github repository.

While most of those tools are standard (with the exception of quartz, but it relies itself on the standard Node and npm technologies), we acknowledge that

  1. It is challenging to understand that many different technologies,
  2. We should strive to welcome contributions from collaborators not familiar with them,
  3. Our set-up is unique in some respects.

This guide tries to alleviate some challenges resulting from this overall unique and diverse resource organization. For more details about our tools, please refer to the Installing dependencies and Repository Maintenance sections.

Locating Resources

To obtain the latest version of this resource, you can either

This resource is an extension of csci-1301.github.io/, please refer to their user guide for more information about it.

Editing Resources

If you are new to this project, first read through Contributing Guidelines to learn how you can contribute to the improvement of this resource, and if applicable, how to join a contributing team.

Best practices for all forms of content

Inclusivity

Follow the IT Inclusive Language Guide from the University of Washington:

use gender-neutral terms; avoid ableist language; focus on people not disabilities or circumstances; avoid generalizations about people, regions, cultures and countries; and avoid slang, idioms, metaphors and other words with layers of meaning and a negative history.

Typically, we recommend using

In doubt, please start by referring to this list of problematic words and phrases.

Structure for accessibility

Resources to assess accessibility:

Markdown

Text documents are written using standard markdown syntax. More precisely,

Because of the way the markdown is processed, please refrain from using the and characters: pandoc will automatically convert " into language-appropriate quotes for us.

Images

Syntax example. The quoted text is the alt tag and in parentheses is path to file

!["image of visual studio IDE"](./img/vs_ide.jpg){ width=80% }

The { width=80% } attribute is optional.

Images generated by LaTeX

Some images are generated by LaTeX: the .tex file is what is used to generate the .pdf file, and then pdf2svg converts the .pdf into a .svg file. The .svg files are used in the .html, .odt and .docx documents, while the .pdf is used in the .pdf documents. The resulting images are added to the repository so that there is no need to re-compile them every time, or to set-up LaTeX and latexmk on each system.

UML class diagrams

The UML diagrams are created using Mermaid and located in source/uml.

Class Diagrams

The class UML diagrams are created using Mermaid and located in source/uml.

To create a new class diagram, say for a Documentation class, follow those steps:

  1. Create a Documentation.txt file in source/uml/cla that follows the syntax for class diagrams (note that there is no need to add classDiagram at the beginning, it will be done automatically),
  2. Run (from the source/ folder) make uml/cla/Documentation.md,
  3. Integrate the resulting drawing, properly captioned and with a link to your Documentation.txt file (for visually impaired readers, or to facilitate automatic processing) using !include uml/cla/Documentation.md.

Source code

Code snippets can be included in markdown documents using pandoc-include filter:

    ```text
    !include code/sample.cs
    ```

Note that for an unknown reason, no special characters (such as _) should be used in the filenames.

Tidying Source code

CSharpier is used to tidy the source code and make it uniform. Use

make tidy

to tidy all the source code present in the source/code/ folders. The configuration file is at source/code/csharpierrc.yaml.

Creating new lectures

Lecture notes belong to the source/lectures/ directory.

To create a new lecture, for instance on exception handling:

  1. Create a directory corresponding to the theme if it does not exist already (say, exceptions), under source/lectures/ directory

    • Follow the existing pattern for naming convention which is lowercase and separation by underscores.

    • At the root of this folder, create an index.md file (so, at source/lectures/exceptions/index.md) containing

      ---
      title: Desired Title for Theme
      ---

      so that your theme will be labeled “Desired Title for Theme” on the website’s menu (see content labelling on how to further label it).

  2. Under the directory corresponding to your theme, create a file named after the lecture’s title (e.g., exception-handling.md) in lowercase. Write lecture notes in this file using markdown.

  3. Edit the source/order file and insert where appropriate

    • ./lectures/exception/ (if you created a folder called exception),
    • ./lectures/exception/exception-handling.md (which must be between ./lectures/exception/ and the next ./lectures/xyz/ folder).

    This last step will insure that your lecture is 1. included in the book, 2. sorted correctly on the website’s menu (the default ordering is alphabetical).

    The order file, unfortunately, does not accept the same folder name twice (even if they are located in different folders or at different levels): there is no good solution to this problem, one has simply to make sure that no two folders have the same name.

If the lecture does not appear, here are the steps for troubleshooting the issue:

  1. Check that after committing changes, the automated build has completed successfully, by checking the workflows,
  2. The newly created lecture is under the subdirectory you picked in the source/lectures/ directory,
  3. The .md file exists,
  4. Hard refresh the browser page if viewing the resources website

Known issues: When concatenating files pandoc may or may not include empty spaces between individual files. This may cause the subsequent lecture title to not appear in the generated book. For this reason, each lecture file should end with a newline.

Creating new labs

The process is very close to the process to create a new lecture, with the following exceptions:

Additionally, remember to:

  1. Choose a short and unique name that describes the lab (say, StringMethods.md)
    • follow the existing convention for naming,
    • do not number labs or make assumptions about numbering because another instructor may not follow the exact same lab order,
    • make the lab standalone to support alternative ordering (avoid assumptions about what was done “last time”),
    • do not make assumptions about student using specific OS, include instructions for all supported options (Windows, MacOS, Linux),
    • do not make assumptions about student using Visual Studio, refer to IDE instead.
  2. (optional) You can add a downloadable project (use a link of the form [the Rectangle project](./code/projects/Rectangle.zip)) or include snippets of code by following our instructions to add source code.

Using this established build system generates labs that are cross-platform (Windows, MacOS, Linux) and work on different IDEs (this process is documented in the corresponding repository). Do not attempt to create labs locally as that approach does not have the same cross-platform guarantee.

Content Labelling

Technically

Quartz support a powerful tagging system which should be leveraged. Markdown files can contain at their very top a YAML metadata block containing, e.g.

---
tags:
- Resource
---

to “tag” this resource with “Resource” so that it will appears in the tag listing. To include multiple tags, simply make a list:

---
tags:
- Resource
- Guide
---

Conceptually

We will follow the guidance provided on this page:

Styling and Templating

Templating files are under source/templates/ directory. Templates directory contain layout files that are applied by pandoc when resources are built: note that the website’s style uses a completely different mechanism.

For maintainability reasons it is preferable to apply templates during build time. This strategy makes it easy to edit templates later and apply those changes across all resources. Avoid applying templating to individual resource files whenever possible.

Currently templates directory contains the following:

Updating docx template

Note that this template is not used yet, for (among other) size issues.

To edit this template, start by obtaining the default template file:

pandoc -o custom-reference.docx --print-default-data-file reference.docx

Then, open reference.docx, and, following loosely this tutorial, do:

This was inspired by this post but does not seem to work properly.

Updating odt template

First, output the default template file:

pandoc -o custom-reference.odt --print-default-data-file reference.odt

Then, open reference.odt, and, following loosely this tutorial, do:

Building locally

It is generally not necessary to build this resource locally unless the intent is to preview templating changes or to make changes to build scripts. For the purposes of editing content, it is sufficient to make edits to markdown files and commit those changes.

Installing dependencies

To find the current list of dependencies needed to build this resource, refer to the build and deploy script install section. The exact installation steps vary depending on your local operating system.

In general the following dependencies are needed:

For this later, note that starting with version 11, the licence is too restrictive for non-personal use. As a consequence, users are asked to make sure they do not use a version greater than v.10.24, which is “free for any use” and archived on-line (curious users can also refer to the related webpage). Note that installing this dependency using a unix-like package manager will result in installing a version of the font that is free to use in any context.

You can make sure you are currently using the latest version of panflute by running

pip install -U panflute

This is needed if running a recent version of pandoc (as of pandoc 3.1.6.1 at least).

Running the build

⚠ Warning
Running make all can be very resource-incentive and may render your system unstable. Read this section entirely before running any command.

Testing the installation

After installing all dependencies, from the source/ folder, run:

make

to display a list of useful rules.

It is recommended to first run a command building simple documents or copying files to test your installation, such as

make ../content/docs/about/credits.md
make ../content/docs/about/credits.pdf
make ../content/docs/about/credits.odt
make ../content/docs/about/credits.docx
make ../content/code/projects/Rectangle.zip
make ../content/web-order.ts
make ../content/img/create_project_monodevelop.png
make ../content/fonts/hack/hack-italic-subset.woff

If this was successful, you can compile the resources needed for the website using

make build-light

Building all resources

You can run

make -l 2.5 -j$(nproc --ignore=2) all

to create and populate the content/ folder at root level with all the resources compiled. Note that this command limits the number of jobs in parallel and the number of CPU used (using this trick), but that tweaking those values may be needed to find the sweet spot on your own machine.

If you want to speed-up the compilation time, you can run

make fetch

which will fetch the latest build output, extract it and populate the content/ folder using its content. Due to make’s unique feature only the files whose source was edited will be re-created when executing make all the next time, hence saving a lot of time. However, please not that files moved or deleted will still be present in the build.

Website

Editing the website

The website https://princomp.github.io/ is built from the .md files contained in the content/ folder using a dedicated branch of quartz. To edit the layout, style, or other features such as the footer, please start by checking out the quartz branch (using git checkout quartz), and then

A couple of indications about the edits made to quartz:

useSavedState: true, // To debug the explorer, change to "false" (this way, the menu is not cached / permanent), 

to false in the quartz/components/Explorer.tsx file may help in refreshing the menu more easily.

Refer to Generate the git patch for instruction on how to generate a patch containing all the edits performed to our local copy of quartz.

Deploying locally the website

Follow closely those steps:

mv content/index.md content/index_b.md

Updating quartz

Our local copy of quartz, in the quartz branch, is “frozen” in the sense that it corresponds to the development of quartz at a point of time. It is possible to

  1. Save the edits made to our local copy (as a git patch),
  2. Pull the current version of quartz in a different branch (called quartz-update),
  3. Apply our edits to this updated version of quartz,
  4. Replace the quartz branch with the quartz-update branch to deploy the updated version of quartz with our edits.

This process is not without risks and requires to be able to deploy locally the website to test it before deploying it. The following guide was inspired by this discussion.

Generate the git patch

The first step is to save as a git patch all the edits that have been made on our local copy of quartz since it was last updated.

Clone the latest version of quartz

Execute the following commands:

git remote add quartz https://github.com/jackyzha0/quartz.git
git fetch quartz
git checkout -b quartz-update quartz/v4

where quartz-update is the name we use for our branch, and quartz/v4 is the name of the branch in the quartz repository we want to copy.

Apply the git patch

There are two ways of applying the patch. First, make sure you are in the quartz-update branch by executing

git rev-parse --abbrev-ref HEAD

Then follows the first method if possible.

Using apply

First, check if the pcp_quartz_patch patch is applicable, by executing

git apply --ignore-space-change --ignore-whitespace --check --reject pcp_quartz_patch

Some sections of the patch may be rejected: make sure you take note of which file will need to be merged by hand. Then, apply the patch, using

git apply --ignore-space-change --ignore-whitespace --reject pcp_quartz_patch

Then look for the .rej files: they will contain the edited version of a file that you will need to merge manually with the updated version of the same file from quartz’s update.

Using patch

If git apply gave an error starting with

Checking patch quartz.layout.ts...
error: while searching for:

then, instead, do

patch -p1 < pcp_quartz_patch

And look for the .rej files as described above. Note that using this technique requires to copy the binary files by hand. Indeed, you should receive warning messages like

File quartz/static/android-chrome-192x192.png: git binary diffs are not supported.

and those files will have to be copied by hand from another branch, and / or re-added to the repository.

Testing

Once you are done manually merging, test your updated version by deploying locally the website and making sure that quartz does not return any error. If everything looks ok, add all the new files and commit the edits using a message containing the “PCP” string (to facilitate future generation of git patch), and push, using for example:

Update the branch

If you were able to fix all the conflicts and to check that the website could still be deployed locally, then overwrite the quartz branch with the quartz-update branch, by executing:

# Make sure your working tree is in a clean state
git status

# Check out the branch you want to change, e.g. some-branch
git checkout quartz

# Reset that branch to some other branch/commit, e.g. target-branch
git reset --hard quartz-update

If the deployment was successful and everything seems to be working, you can delete the quartz-update branch, locally then remotely, by executing

git branch -D quartz-update
git push -d origin quartz-update

Repository Maintenance

This repository uses following tools and technologies:

Build outputs

The resource material is organized into specific directories inside the source/ folder. These resources are then compiled into templated documents in various formats using pandoc. The makefile explains the exact steps applied to each type of resource.

Github actions

This resource is built automatically every time changes concerning files in the source/ folder are committed to the main branch of the repository. This is configured to run on Github actions. The workflow that is automatically triggered has two jobs: one to build the resource, and one to deploy it.

Currently Github actions offers unlimited free build minutes for public repositories (and 2000 min/mo. for private repositories, should we ever need them), which hopefully continues in perpetuity (if it does not there are other alternative services). Going with one specific CI service over another is simply a matter of preference.

Following a successful build, the build script will automatically deploy the generated resources to an accompanying website hosted on github pages.

Fetch and No Fetch Versions

There is a second workflow that is identical to the first one with one important exception: to speed up compilation, build_and_deploy.yaml uses make fetch to speed up compilation time by re-downloading the latest build output, and then compiling only the required files. This can sometimes complicate the propagation of changes, typically if a template is modified (as this does not triggers a re-compilation of the files using it currently) or if a file is renamed (as the previous version will not be deleted).

The build_and_deploy_no_fetch.yaml can be triggered manually to force a “fresh” remote compilation.

Creating releases

Currently a github action is setup to do the following: whenever a new commit is made to the main branch, the action will build the resource and add the generated resources as a pre-release and tag them as “latest”. If a subsequent commit occurs it will overwrite the previous latest files and become the new latest version. This cycle continues until maintainers are ready to make a versioned release (or “package”).

Making a versioned release is done as follows:

  1. Go to repository releases
  2. Choose latest, which contains the files of the latest build
  3. Edit this release, giving it a semantic name and a version, such as v1.0.0. Name and version can be the same. (cf. semantic versioning)
  4. Enter release notes to explain what changed since last release
  5. Uncheck “This is a pre-release”
  6. Check “Set as the latest release”
  7. Update release

Following these steps will generate a new, versioned release. The versioned releases will be manually uploaded to and archived on galileo.

Once this is done, remember to create the next pre-release:

  1. Go to the repository releases.
  2. Click on “Draft a new release”.
  3. Pick the tag “Latest”.
  4. Click on “Generate release notes”
  5. Check “This is a pre-release”
  6. Click on “Publish release”

Maintaining repository feedback

Resource users can submit feedback about the resource through various means, one of which is leaving comments on the website. This feature is enabled by utteranc.es, using repositories hosted by the princomp github organization.

To manage user feedback over time, a semester-specific repository is created for issues only. This must be a public repository and located under the same organization as the resources repository. utteranc.es widget is configured to point to this repository. After a semester is over, this feedback repository will be archived, and a new one created for the next semester. This will simultaneously archive all older issues and reset the feedback across website pages.

Migrating feedback repository

The steps for migrating feedback target repository are as follows:

  1. Create a new public repository under princomp github organization. Follow the established naming convention (feedback-<fall|spring|summer>-<YYYY>), and leave all the options except for visibility (which needs to be set to public) by default.

  2. Go to repository Issues (make sure issues is enabled in repository settings).

  3. Create a new label whose label name is comment (to match widget configuration as indicated in quartz/components/Footer.tsx, in the quartz branch).

  4. Go to Organization Settings > Installed GitHub Apps.

  5. Choose “utterances” > “configure”

  6. Under “Repository access” > “Only select repositories”

    • Select the repository created in step 1.
    • Remove the previous semester feedback repository.
    • Save.
  7. In princomp/princomp.github.io/ repository, in the quartz branch, open quartz/components/Footer.tsx

  8. Update utteranc.es widget code to point to the new feedback repository created in step 1.

    <script data-external="1"
            src="https://utteranc.es/client.js"
            repo="princomp/{REPOSITORY_NAME}"
            label="comment">
    </script>
  9. Commit change to quartz/components/Footer.tsx

  10. Make sure the feedback works after migration. If it does not, retrace your steps.

  11. Archive the earlier feedback repository in its settings.

Maintaining Instructors / G/UCA rights

This is handled by the csci-1301 github organization and documented at https://csci-1301.github.io/user_guide.html#maintaining-instructors-guca-rights.

How to get Help

🛈 Note
This page is primarily targeted for Augusta University students.

This page lists resources for Augusta University students to receive help with their course of studies, in general, for students of the School of Computer and Cyber Sciences, and for this course in particular.

In General

Many resources are available to help you be a successful student:

For Students of the School of Computer and Cyber Sciences

School of Computer and Cyber Sciences Tutoring Center

The School has a tutoring center that can be reached:

ACM Club

The Augusta University chapter of the A.C.M is one of the university’s best resources for Computer Science, Information Technology and Cyber Security students. It provides a platform to network with other students in similar majors, presenting countless opportunities to not only expand the people you know, but also a fantastic place to learn and ask questions. To learn more, you can sign up for the newsletter, or attend one of the subgroup meetings (meeting times and locations are listed on the website).

Other Club Activities

The Augusta University Game Design Club and Girls Who Code College Loop “will be continuing activities in full force this year”. Notifications for upcoming activities will be shared in class alongside school-wide emails.

How to Ask a Question?

It may seems silly, but asking a question “the right way” may not always be easy.

  1. Once you’ve identified your issue, try again from scratch to see if you missed a point.
  2. Go over the instructions, and look in our resources for some meaningful keywords.
  3. Think about how you can describe your issue, what is the shortest route to reproduce it.
  4. If you are still facing difficulties, be detailed and clear about what you think went wrong: if the question is related to computers, specify which operating system, what you have tried, the exact nature of the error message, etc. Screenshots are not always the right way to convey your question: try to be descriptive, and explain what you tried. If you want to refer to a particular lab or lecture, open the corresponding page, look for the closest title, hover over it, and you should see a “§” symbol appears: click on it, you can now share that link so that your interlocutor knows precisely what you are talking about!

And, remember: your instructor(s) knows that you are a student and here to learn, so you should never feel intimidated or assume that everyone knows better than you: many students struggle in this class at times, and you could actually do them all a favor by asking your instructor(s) to go over a particular dimension that they may have overlooked or explained poorly!

Commenting Using a Github Account

On this website, if you look below, you will see a box where you can comment. This will require that you create a Github account, which is free and may serve multiple purpose if you intend to study, use, or contribute to open-source projects. The comment can use the markdown syntax (exactly like this resource!), which is also used on websites like stackoverflow and extremely popular!

Choosing Your Major

🛈 Note
This page is primarily targeted for Augusta University students.

Which degree is best for you?

Most universities offer both a Computer Science degree and an Information Technology degree, and some universities even offer a Management Information Systems degree. Here at Augusta Unversity, we have all three options for you:

along with two unique diploma,

While all of these degrees are high-quality and should place students on a fast-track towards a successful career, students always ask the same question, “Which degree is best for me?” The answer to this question depends on the student, their career goals, and a variety of other factors.

Students even ask more specific questions:

These are all great questions! But before answering them, it is more important to have a basic understanding of the degree options.

The following links detail these three degrees and explain the benefits of each:

Additionally, Augusta University has more information on its advising page. To answer the first question (“Which degree will give me the highest salary?”), you can use Georgia Degrees Pay

Summary

Computer scientists design and develop computer programs, software, and applications. IT and IS professionals then use, configure, and troubleshoot those programs, software, and applications.

So it really depends on what you want to do. Do you want to be on the front end, designing the software and applications? Do you prefer to use and troubleshoot them? One of the websites gave the analogy of a home: computer scientists build the home, set up home, install the lighting, plumbing, etc., and then the IT/IS professionals come and live in the home to use it, test it, and troubleshoot it.

So which degree is “best”?

Perhaps you can now see how this question is not fair or at least not clear. If we ask which degree is more difficult, the students will immediately exclaim, “Computer Science is the most challenging!” Therefore, one can perhaps argue that the Computer Science degree is the most rigorous (challenging) and will likely provide the student with more opportunities in their career. And the salary statistics support this argument, as CS students, on average, have a higher salary than their IT and IS colleagues.

That said, is Computer Science better? Yes, and no. It depends on you! It depends on your goals. It depends on how hard you want to work. For some, “better” means more money and more career opportunities. For others, “better” means easier studies and less math! So again, which degree is “best”? There is no short answer. As mentioned above, all three degrees provide the tools you need to hopefully have a great career. Perhaps the question is best worded as, “Which Degree is Best for me?” And of course, only you can answer this question!

Course Assistants

🛈 Note
This page is primarily targeted for Augusta University students.

What Is an Undergraduate Course Assistant?

In this course, an Undergraduate Course Assistant (UCA) is generally present in addition to your instructor. A UCA is a student, generally in the School of Cyber and Computer Sciences, who successfully passed CSCI 1301 and that is hired by the School to assist other students.

Their duties generally include:

Their duties can not include:

How Do I Become One?

A UCA is hired by the School upon recommendation of instructors, after discussion with our Academic Program Coordinator, and possibly our Director of Undergraduate Studies.

A UCA must:

Additionally, if a student wants to help with this particular class, then the student must have successfully passed CSCI 1301 with a grade of B or higher

A UCA will:

So, in short: talk to any CSCI 1301 instructor if you feel like becoming a UCA.

I Am a UCA, What Should I Do Now?

Congratulations! You should now read more about your position in the UCA starting guide!

What Is the Difference With a GRA?

Graduate Course Assistants (GRA) hold a bachelor and are generally PhD or Master student. Their duties generally overlap with those of the instructors and those of the UCAs, as they are the first point of contact of UCAs, design projects, organize the schedule of the tutoring center and of the labs.

What Is the Difference With a URA?

Undegraduate Research Assistants (or “URAs”) share many similarities with UCAs:

However, their focus is on working on research instead of being focused on teaching. The difference is sometimes tenuous, but URAs positions are generally given in priority to “advanced” students (that is, close to graduation), to use their gained knowledge to push further the limits of human knowledge!

It is not possible to cumulate an URA and an UCA position, but obtaining an UCA position is in general an excellent stepping stone to obtain a URA position, if you wish to do so: by proving that you are reliable, serious, agreeable to work with, you will maximize your chances of having a Faculty member notice you and offer you to work on their research with them.

UCA starting guide

🛈 Note
This page is primarily targeted for Augusta University students.

Congratulations on your new position! This page briefly explain what is expected from you as an Undergraduate Course Assistant (UCA).

The Three Rules

There are three important rules for you:

  1. This is a job.

    Meaning that you have a contract that you should have read and understood, and that you need to carefully clock in and out to receive the pay you deserve. Briefly reviewing the information listed here, and in particular those slides can help you in making sure that you understand all aspects of your position. Do not forget that you are first and foremost a student, and that your main goal here is to graduate.

  2. You are here to help students, not to solve their problems.

    Please, review what you should and should not do on this section. It is difficult to strike the right balance when helping a student, but a good rule of thumb is that you should not do anything yourself, just explain and give hints so that they can solve the problem they are facing. You are here to help students understand how to solve a problem, not to solve it for them.

  3. Don’t hesitate to ask.

    That’s it. You are not alone to deal with difficult situations (cheating, rude behavior, student abusing your time, etc.), and it is normal if you are sometimes unsure of the best course of action. The instructors are happy to train you and help you solve problems that may arise.

In general, UCAs should prioritize giving clear and concise explanations and hints, as to avoid confusion while also helping them better understand the problem-solving process. This means that when you encounter a problem that you are not able to solve, it’s important to ask a colleague who is available for help and try to understand their approach. This way, the student can receive assistance more quickly and will be less likely to get confused during the troubleshooting process. By emphasizing the importance of understanding and working through the problem, rather than just providing a solution, tutors can help students develop the skills they need to become more independent problem-solvers.

On top of supporting students and helping the instructor, you are also encouraged to work on the improvement of those resources. Your contribution may range from spell-checking to pointing inconsistencies, from clarifying statements to re-organizing exercises. Thanks to git and pull requests, you do not need to worry (too much) about introducing mistakes or blunders: the changes you suggest will always be reviewed by instructors before being merged in our master document. We discuss below how you can edit our resources.

Editing the Resources

You need three things to start editing our resources:

Follow the instructions in our “Contributing Guidelines” for the first step.

For a quick syntax guide in Markdown, the best resource is this website and its 10 minutes tutorial. We list some best practices, and would appreciate if you could follow them.

For github’s interface, please refer to the following guide (where the screenshots where taken for the csci-1301.github.io website, but remains relevant).

“Navigating repositories”

GitHub is separated into many “repositories”:

“Navigating folders”

Under the Code section (next to Issues, Pull Requests, Actions, etc.), you will find various folders containing documents for the website. Typically, if there is some error or mistake in the lecture notes, so that will be where you will navigate to the most. The way the resources are organized is explained here.

“Navigating documents”

For this example, I just clicked on the first chapter, “General Concepts”.

“Editing Mode”

On this page, you can see the edit history of that specific document you clicked on. In the corner above the document and below the edit history, there is a pencil icon that will put you into editing mode for that document.

“Editing vs Previewing”

On this page, you will see the document formatted as markdown with two sections at the top of the document: Edit file and Preview. If you have Edit file selected, then you will see the “code” version of the document whereas if you click on the Preview button, you will see the document in its “final” form, or how the website users should see it, without the “code”. To edit, make sure you have Edit file selected.

“Proposing Changes”

Once you have made the edits you wanted, you need to “commit” them; just like how you may write a paper, you need to submit it to the professor for them to see it. At the bottom of the page, there is a header box and a description box for you to describe what you did so others will know the changes you did (you do not need to go into every detail; just describe it generally, like “I fixed grammatical issues” or “Fixed code error”). As a UCA, you do not have write access to the princomp.github.io repository, so submitting a change will write it to a new branch in your fork <your name>/princomp.github.io, so you can send a pull request. Given the new protocol by Github, after making the neccessary edits, click the “Propose Changes” button located at the bottom. On this page and the next, there will be a “Create pull request” button, by clicking on this you will start a pull request. After you have successfully created a new branch for your commit and started a pull request, your edits will be checked by others so as to catch any mistake(s) you may have introduced before your pull request is merged into the base branch.

“Committing”

Note that if you are making edits inside the repository for UCAs, uca-resources-<semester>-YYYY, you do have write access so there will instead be two buttons: Commit directly to main branch and Create a new branch for this commit and start a pull request

You can Create a new branch for this commit and start a pull request so others can double check your edits: it can act as a safety net, so your colleagues will be able to catch any mistake(s) you may have introduced!

Computer Requirements

🛈 Note
This page contains some recommendations on students wishing to buy a computer to complete their program in the School of Cyber and Computer Sciences. Note that possessing a computer is not required to complete CSCI 1301, but recommended.

In Short

Anything less than 5 years old running Microsoft Windows, macOs or a Linux operating system is probably fine. Second hand and custom built are fine, but you will in all likelihood needs a portable computer (as opposed to a desktop computer) to present your work and work on projects.

In Terms of Hardware

Desktop, Laptop, or something else?

A laptop is generally recommended (to take notes in class, make presentations, work on projects at School, …) but technically possessing only a desktop should be ok (and will be more comfortable to use, in all likeliness). Tablets and other “small” handled devices (such as Netbooks, Chromebooks or Mini PCs) are not recommended and will in all likelihood prove challenging to use for some classes.

Specifications

Component Minimum Suggested Comfortable
CPU 4 cores @ 2.66 GHz 6 cores @ 3.8 GHz 6 cores @ 4.4 GHz
RAM 8GB 16GB 32GB
Hard Drive 100GB 500GB of SSD 1TB of SSD

GPU and other special equipment are not required, but recent USB-C connectors will be useful.

As An Example

Dr. Aubert uses a Dell Latitude 5480/5488 from 2017 (but in no way endorses it) with

and of courses wishes that it was a bit more responsive at times, but can conduct otherwise all his professional activities.

In Terms of Operating System

We will briefly consider four “families” of operating systems:

Note we do not discuss Android or iOS since they are primarily mobile operating systems, and not easily suited for the development workload in our curriculum.

In Short

Anything but ChromeOS is (probably) fine.

Expanded

Virtual Machines

Virtual machines allow you to simulate (almost) any operating system using (almost) any operating system: this means that, for instance, you can load the Windows 11 operating system from your computer running Debian 12.5, or the Debian 12.5 operating system from macOS 14.

Note that CSCI 4532 - Hardware and Embedded Systems and CSCI 4531 - Malware Analysis and Reverse Engineering require you to run virtual machines. If you are planning on taking one of those classes, make sure your computer can run virtual machines!

You can find on this page some indications on how to run a virtual machine on your computer, and you can check on-line the recommended specifications for Hyper-V, VirtualBox, kvm, vmware. Note that, as a student, you can obtain a free licence for Windows.

Where to Buy?

That is really up to you, but remember that, as a student (or employee), you are allowed to

Second-hand computers or even custom-built computers are probably fine, but requires more skills (such as how to factory-reset a computer and / or how to (re)install an operating system) and inspections on your end.

Is There Anything Else I Should Know?

Installing Software

Generalities on Installing Software

You probably already installed software in your life, be it VLC, Microsoft Teams, or Whatsapp. However, depending on whether you installed it on a phone, a tablet, a computer, and depending on the operating systems (Android, Windows 10, iOS, Ubuntu, etc.) your experience may have varied drastically.

Between the Play store, the command-line interface, homebrew and the act of downloading software using your browser and then installing it using the navigator, there can be a lot of differences, but in all those circumstances you should keep security in mind. In addition to making sure that you are downloading the software from a trusted source, you should also be vigilant about the information the software will be able to access about e.g., your private life.

As data can be lost or corrupted upon downloading, many platforms now use checksums to verify the integrity of the software you downloaded before installing it. This is an excellent practice that can also be performed “by hand”, as explained for instance for the database manager MySQL: the main idea is that the probability of the signature matching a tampered-with file is extremely low, and that as long as you are downloading the signature and the software from two different sources, you are considerably reducing the attack surface.

Executing Code Found on-line

As you progress in this class, you will be asked more and more to download and execute code hosted in our repository. How can you tell that you can trust this code?

We have not implemented checksum-matching (yet!), but you can trust this code as it was coded by your instructors, and hosted on a platform using two-factor authentication where every action is tracked using versioning. Concretely, this means that only somebody who manages to steal your instructor’s credentials and their phone, and thwart all the other instructors’ vigilance, would be able to host malicious code on our platform: while we certainly imagine that this is theoretically possible, we hope that you will agree that the probability is low enough for you to trust the code on this site.

As often, security is not absolute, but aims at providing reasonable confidence. Executing “blindly” code found on-line, on the other hand, gives you a good chance of facing unpleasant surprises: while there certainly is a lot of useful, good code on websites like stackoverflowyour instructor probably uses such websites, by the way!, copying-and-pasting it without understanding its purpose or general structure is almost guaranteed to, at best, not execute properly, at worst, make your system unstable or insecure.

Accessing an IDE

An IDE, for “Integrated development environment”, is the software or service you will be using to write, compile, execute and debug your code. There are many available IDEs, and some can accommodate multiple different programming languages.

For C#, there are many different possibilities: some are cross-platforms (meaning you can use them on macOS, Windows or Linux), some are provided free of charge, some have not been updated in a long time. Three natural choices are Visual Studio, MonoDevelop and Rider. While the last two are accessible on every operating systems, Visual Studio is available only for Windows, and in a slightly different version for macOS.

To access one or the other, you will need either

The third solution is a backup plan, as instead you will access a very minimal version of an IDE to test small snippets of code. You should not rely on it for the duration of this course.

Installing an IDE On Your Own Computer

This part gathers some references for you to install Visual Studio, MonoDevelop and Rider on your own computer, regardless of your operating system. It is strongly encouraged that you do so, especially if you want to continue in a CS/IT/Cyber degree, but is not mandatory3.

The instructions are detailed, but there are plenty of ways this can go wrong: make sure you have read and followed those instructions carefully before asking for help!

Installing Visual Studio On Your Own Computer

Note that we are not installing “Visual Studio Code”, but simply “Visual Studio”.

For Windows
  1. Visit Azure Dev Tools for Teaching.

  2. Log in using your Augusta University credentials.

  3. Select “Download software”.

  4. Look for Visual Studio. The path is Education → Software → Visual Studio Enterprise 2019/2022. You can search “Services” for the “Education” group and then click “Software” if the education group is not immediately displayed. It should look like the following:

    Normally, the following direct link should get you to the right page: https://portal.azure.com/?Microsoft_Azure_Education_correlationId=8ee63052-dc32-46f7-a109-e26793622dbf#view/Microsoft_Azure_Education/EducationMenuBlade/~/software. Type “Visual Studio Enterprise” in the search bar and you should find what you are looking for:

  5. Download and install Visual Studio (leave all the options on their default settings).

    Before clicking install, make sure to check “.NET Desktop Development”

    If you are installing Visual Studio 2019, click the dropdown for .NET Desktop Development and check “.NET SDK (out of support)”. You do not have to do this for Visual Studio 2022

  6. Enter the product key you obtained previously, following the instructions in the documentation. Normally, clicking on “View key” on the screen pictured in the fourth step above should give you access to a key, that you simply need to copy-and-paste in the menu you can access on Visual Studio by clicking on “Select File” → “Account Settings” → “License with a Product Key”.

For Mac

Download a version of Visual Studio at https://visualstudio.microsoft.com/vs/mac/. It differs a bit from the Windows version, but that should not impact your experience in this class. The only Visual Studio feature we rely on is the ability to create “Console Apps with C#”, which is equally available in both the Windows and Mac versions.

Installing MonoDevelop On Your Own Computer

Unfortunately, MonoDevelop offers pre-packaged release only for linux distributions

Installing Rider On Your Own Computer

You can download Rider from their website, for any operating system. Note that, as a student, you can obtain a licence for free: simply fill out this form, making sure you use your @augusta.edu email account, and you should receive a free licence instantaneously!

Note that Jetbrains offers to use a SHA-256 checksum (for instance, for the linux version) for you to check that your download has not been tampered with. In any case, you can consult their detailed instructions to install and execute Rider on any operating system.

Installing a Code Editor On Your Own Computer

IDE in general performs the operation of setting up the compiler for you, but if you are willing to try to do it yourself, you can then access a larger offering of editors. Indeed, an alternative to installing an IDE is to install a C# compiler on one hand, and a code editor on the other hand (which is just a text editor with some completion or visualization related to the programming language you are using).

Among other code editors suited for C# code, we can mention:

We give below some indications on how to set-up Geany.

Installing Geany On Your Own Computer

Note:
This method will only allow you edit and compile individual .cs files, and will not compile C# Solution Projects. To set-up Geany so that you can compile projects, could start by reading this exchange (which is about projects in Linux, but applies equally well to projects in C#) or this one.

You can download Geany from their website, for any operating system. To use Geany as a text editor for C#, we must download the Mono C# compiler from their website. Make sure to download the most recent version to assure your compiler has the most up-to-date version of “.NET”.

Once you installed Mono, locate the “csc.bat”, “csc.exe” or “csc” file in Mono’s “bin” folder and copy the file path. This path can be of the form

C:\Program Files (x86)\Mono\bin\csc.bat

on windows, or

/usr/bin/csc

on Unix systems.

Now open a .cs file using Geany. Click the arrow next to the “Build” Button and click “Set Build Commands” from the dropdown menu.

Accessing the menu to set build commands

In the “Set Build Commands” window, erase the entry next to the “Compile” button and paste the file path to the “csc.bat” in quotation marks. After the file path, create a single space followed by “%f” with the quotaion marks. All in all, you should have something of the form

"C:\Program Files (x86)\Mono\bin\csc.bat" "%f"

in the “Command” field of the “Compile” line.

Confirm the change by clicking OK and now you will be able to compile, build, and execute standalone .cs files.

Setting the build commands

Installing Anything Anywhere

If the IDE you would like to adopt is not available for your operating system, you can use a Virtual Machine manager to execute a linux-based distribution or a Windows image on top of your operating system.

For this, and regardless of your current operating system, you will need a Virtual Machine manager.

  1. There are many (free) options to chose from, let us mention
    1. Virtual Box (for Windows, Linux and Mac),
    2. QEMU (for Windows, Linux and Mac),
    3. Hyper-V (for Windows),
  2. Download a version of “Microsoft Operating Systems” from Azure Dev Tools for Teaching, or a linux-based distribution (typically, ubuntu has a good reputation of being accessible and user-friendly).
  3. Install and execute your version of Windows or Linux from your virtual machine, and follow the corresponding instructions to install the IDE you are interested in.

Note that it is illegal to execute macOS in a virtual environment that is not hosted on a mac computer, which drastically reduces the interest for you to consider this option.

Accessing One of the Computers in a Computer Lab

Please refer to this page from AU’s Information Technology to know where the computer labs are located. Visual Studio should be pre-installed on every computer.

Compiling Code On-Line

As a backup or only to test snippets of code, you can compile C# code online. Multiple online platforms exist, such as:

Note that none of them are endorsed by the school and that they can pose security and privacy challenges: never enter any sensitive information and do not rely on them too heavily. However, they can be a good support if you would like to test a short snippet of code but do not have access at the moment to a computer with an IDE installed.

(Un)Zipping Archives

This short note explains how to

for the three main operating systems (Windows, Linux and macOS).

Unzipping Files

Windows

Navigate your file explorer and navigate to your Downloads folder (or wherever you downloaded the file). From there, look for the file you downloaded, right-click, and select “Extract All…”. When the “Extract Compressed (Zipped) Folder” window opens, click the “Extract” button.

Linux

This guide is assuming you have Zip/Unzip installed on your Linux distribution. If this is not the case, first follow this install guide.

Using the graphical interface

Normally, a simple right click and choose “Extract” or “Open with Ark” should do it.

Using the Command-Line

Navigate to your command-line interface and execute the following command (as a normal user, as indicated by $):

$ unzip [FileName].zip

where “[FileName].zip” is the name of the zip file.

macOS

Simply double-click on the zip file to unzip it onto your desktop.

Zipping Files

Windows

Navigate to your file explorer and go to where your solution is stored on your system, the default file path being:

C:\Users\[UserName]\source\repos

where “[UserName]” is your Windows username (on school computers, this should be your AU username). Right click the folder you want to zip, go down the list to the “Send to” option, and then click on the “Compressed (Zipped) Folder” option. This should then create a new zip file.

Linux

Using the graphical interface

Normally, a simple right click and choose “Compress” should do it.

Using the Command-Line

Navigate to your command-line interface and execute the following command (as a normal user, as indicated by $):

$ zip -r [ZipFileName].zip [FileName]

where “[ZipFileName].zip” is the name you want for the zip file, and “[FileName]” for the folder you want to zip.

macOS

Navigate to your file explorer and go to where your solution is stored on your system, the default file path being:

[UserName]\source\repos

where “[UserName]” is your Mac username. Right-click on the folder that you want to zip up and click on the “Compress the Folder” option.

But Where Is My Project?

By default, it should be stored in a folder located in

C:\Users\[UserName]\source\repos

for Windows users,

[UserName]\source\repos

for macOS users,

/home/[UserName]/Projects

for Linux users.

When in doubt, open your project in the IDE, right-click on the solution, and look for an option called “Open in File Explorer” or “Open Containing Folder”:

Keyboard Shortcuts

Foreword

This document contains useful keyboard shortcuts for different operating systems and IDEs. We use the following symbols:

Symbol Common Name
Shift
Option (or Alt)
Command (or Cmd)
(Carriage) Return

The sections labeled with the star symbol (“*”) work generally everywhere, beyond your IDE.

More advanced shortcuts may be available to your particular IDE:

Useful Shortcuts

Build solution

OS Keys
Linux Ctrl + + B
MacOS + B
Windows Ctrl + + B

Exit any program*

OS Keys
Linux Alt + F4 or Ctrl + q
MacOS + q
Windows Alt + F4

Redo*

OS Keys
Linux Ctrl + y
MacOS + y
Windows Ctrl + y

Run/execute program

OS Keys
Linux Ctrl + F5
MacOS F5 -or- + +
Windows Ctrl + F5

Save*

OS Keys
Linux Ctrl + s
MacOS + s
Windows Ctrl + s

Save All*

OS Keys
Linux Ctrl + + s
MacOS + + s
Windows Ctrl + + s

Undo*

OS Keys
Linux Ctrl + z
MacOS + z
Windows Ctrl + z

Comment Code Selection

OS Keys
Linux Ctrl + k + c
MacOS + k + c
Windows Ctrl + k + c

Uncomment Code Selection

OS Keys
Linux Ctrl + k + u
MacOS + k + u
Windows Ctrl + k + u

Datatypes in C

Value Types

Numeric

Signed Integer

Type Range Size
sbyte -128 to 127 Signed 8-bit integer
short -32,768 to 32,767 Signed 16-bit integer
int -2,147,483,648 to 2,147,483,647 Signed 32-bit integer
long -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 Signed 64-bit integer

Unsigned Integer

Type Range Size
byte 0 to 255 Unsigned 8-bit integer
ushort 0 to 65,535 Unsigned 16-bit integer
uint 0 to 4,294,967,295 Unsigned 32-bit integer
ulong 0 to 18,446,744,073,709,551,615 Unsigned 64-bit integer

Floating-point Numbers

Type Approximate Range Precision
float ±1.5e−45 to ±3.4e38 7 digits
double ±5.0e−324 to ±1.7e308 15–16 digits
decimal (-7.9 x 1028 to 7.9 x 1028)/(100 to 1028) 28–29 significant digits

Logical

Type Possible Values Size
bool true, false 8-bit

Character

Type Range Size
char U+0000 to U+ffff Unicode 16-bit character

Literals

Name Corresponding datatype Examples
Integer Literal int 40, -39, 291838, 0, …
Float Literal float 3.5F, -43.5f, 309430.70006F, …
Double Literal double 28.98, 239.0, -391.089, 0.0, …
Decimal Literal decimal 8.95m, 3283.9M, -30m, …
Boolean Literal bool true, false
Character Literal char 'Y', 'a', '0', '\n', '\x0058', '\u0058', …

Compatibility

This table is to be read as

means that those values or variables from the datatypes in the row and column can be “operated together” (meaning, you can for instance multiply them), ✘ means that those values or variables from the datatypes in the row and column cannot be “operated together” (meaning, you cannot for instance multiply them).

  Integer Literal Float Literal Double Literal Decimal Literal
int
float
double
decimal

Result Type of Operations

  int float double decimal
int int float double decimal
float float float double illegal
double double double double illegal
decimal decimal illegal illegal decimal

This table is to be read as

Values or variables from the datatypes in the row and column can be “operated together” and will produce the datatype indicated in the cell, or cannot be “operated together” if the value in the cell is “illegal”.

References

Computers and Programming

Principles of Computer Programming

Programming Language Concepts

We begin by discussing three categories of languages manipulated by computers. We will be studying and writing programs in high-level languages, but understanding their differences and relationships to other languages4 is of importance to become familiar with them.

A Visual Representation of the Relationships Between Languages

A more subtle difference exists between high-level languages. Some (like C) are compiled (as we discussed above), some (like Python) are interpreted, and some (like C#) are in an in-between called managed.

A Visual Representation of the Differences Between High-Level Languages

Software Concepts

Programming Concepts

Programming workflow

Flowchart demonstrating roles and tasks of a programmer, beta tester and user in the creation of programs.

The workflow of the programmer will differ a bit depending on if the program is written in a compiled or an intprepreted programming language. From the distance, both looks like what is pictured in the the flowchart demonstrating roles and tasks of a programmer, beta tester and user in the creation of programs, but some differences remain:

Interpreted languages have

(Integrated) Development Environment

Programmers can either use a collection of tools to write, compile, debug and execute a program, or use an “all-in-one” solution called an Integrated Development Environment (IDE).

IDE “bundle” all of those functionality into a single interface, to ease the workflow of the programmer. This means sometimes that programmers have fewer control over their tools, but that it is easier to get started.

In particular, Visual Studio is an IDE, and it uses its own vocabulary:

C# Fundamentals

Introduction to the C# Language

The Object-Oriented Paradigm

First Program

It is customary to start the study of a programming language with a “Hello World” program, that simply displays “Hello World”. It is a simple way of seeing a first, simple example of the basic structure of a program. Here’s a simple “hello world” program in the C# language:

Hello World

/* I'm a multi-line comment,
 * I can span over multiple lines!
 */
using System;

class Program
{
  static void Main()
  {
    Console.WriteLine("Hello, world!"); // I'm an in-line comment.
  }
}

Features of this program:

Rules of C# Syntax

Conventions of C# Programs

Note that some of those conventions are actually rules in different programming languages (typically, the last two regarding code files are mandatory rules in java).

Reserved Words and Identifiers

Write and WriteLine

Escape Sequences

Datatypes and Variables

Datatype Basics

Literals and Variables

Literals and their types

Variables overview

Variable Operations

Declaration

Assignment

Initialization (Declaration + Assignment)

Assignment Details

Displaying

On a final note, observe that you can write statements mixing multiple declarations and assignments, as in int myAge = 10, yourAge, ageDifference; that declares three variables of type int and set the value of the first one. It is generally recommended to separate those instructions in different statements as you begin, to ease debugging and have a better understanding of the “atomic steps” your program should perform.

Format Specifiers

Variables in Memory

Sizes of Numeric Datatypes

Summary of numeric data types and sizes:

Type Size Range of Values Precision
sbyte 1 bytes −128…127 N/A
byte 1 bytes 0…255 N/A
short 2 bytes −215…215 − 1 N/A
ushort 2 bytes 0…216 − 1 N/A
int 4 bytes −231…231 − 1 N/A
uint 4 bytes 0…232 − 1 N/A
long 8 bytes −263…263 − 1 N/A
ulong 8 bytes 0…264 − 1 N/A
float 4 bytes ±1.5 ⋅ 10−45… ± 3.4 ⋅ 1038 7 digits
double 8 bytes ±5.0 ⋅ 10−324… ± 1.7 ⋅ 10308 15-16 digits
decimal 16 bytes ±1.0 ⋅ 10−28… ± 7.9 ⋅ 1028 28-29 digits

Value and Reference types

Operators

Arithmetic Operators

Variables can be used to do math. All the usual arithmetic operations are available in C#:

Operation C# Operator C# Expression
Addition + myVar + 7
Subtraction - myVar - 7
Multiplication * myVar * 7
Division / myVar / 7
Remainder (a.k.a. modulo) % myVar % 7

Note: the “remainder” or “modulo” operator represents the remainder after doing integer division between its two operands.
For example, 44 % 7 = 2 because 44/7 = 6 when rounded down, then do 7*6 to get 42 and 44 - 42 = 2.

Arithmetic and variables

Compound assignment operators

Statement Equivalent
x += 2; x = x + 2;
x -= 2; x = x - 2;
x *= 2; x = x * 2;
x /= 2; x = x / 2;
x %= 2; x = x % 2;

Increment and Decrement Operators

Increment and decrement basics

int myVar = 1;
myVar = myVar + 1;
myVar += 1

These two lines of code have the same effect; the += operator is “shorthand” for “add and assign”

myVar++;
++myVar;
int myVar = 10;
myVar = myVar - 1;
myVar -= 1;
myVar--;
--myVar;
Increment Decrement
Postfix myVar++ myVar--
Prefix ++myVar --myVar

Difference between prefix and postfix

int a = 1;
Console.WriteLine(a++);
Console.WriteLine(a--);
int a = 1;
Console.WriteLine(++a);
Console.WriteLine(--a);

Using increment/decrement in expressions

int a = 1;
int b = a++;
int c = ++a * 2 + 4;
int d = a-- + 1;

Arithmetic on Mixed Data Types

Implicit conversions in math

Explicit conversions in math

Order of Operations

Conversions

We now discuss implicit and explicit conversions between datatypes: how C# can (or not!) convert a value from one datatype to another, and how we can “force” this conversion if C# does not do it automatically.

Assignments from different types

int myAge = 29;
double myHeight = 1.77;
float radius = 2.3f;

Note that 1.77 is a double literal, while 2.3f is a float literal

Implicit conversions

int length = 2;
float radius = length;

When the computer executes the second line of this code, it reads the variable length to get an int value 2. It then implicitly converts that value to 2.0f, and then assigns 2.0f to the float-type variable radius.

Type Possible Implicit Conversions
short int, long, float, double, decimal
int long, float, double, decimal
long float, double, decimal
ushort uint, int, ulong, long, decimal, float, double
uint ulong, long, decimal, float, double
ulong decimal, float, double
float double

Explicit conversions

float radius = (float) 2.886;

The variable radius will be assigned the value 2.886f.

double length = 2.0;
int height = (int) length;

The variable height will be assigned the value 2.

decimal myDecimal = 123456789.999999918m;
double myDouble = (double) myDecimal;
float myFloat = (float) myDouble;

In this code, myDouble gets the value 123456789.99999993, while myFloat gets the value 123456790.0f, as the original decimal value is rounded to fit types with fewer significant figures of precision.

decimal fromSmall = (decimal) 42.76875;
double bigDouble = 2.65e35;
decimal fromBig = (decimal) bigDouble;

In this code, fromSmall will get the value 42.76875m, but the program will crash when attempting to cast bigDouble to a decimal because 2.65 × 1035 is larger than decimal’s maximum value of 7.9 × 1028

Summary of implicit and explicit conversions for the numeric datatypes:

“Implicit and Explicit Conversion Between Datatypes”

Refer to the “Result Type of Operations” chart from the cheatsheet for more detail.

Inputs and Outputs

Reading Input from the User

Parsing user input

More detail on the Parse methods

Correct input formatting

Output with Variables

Converting from numbers to strings

The ToString() method

String Concatenation

Output with concatenation

Introduction

Class and Object Basics

Writing Our First Class

The Rectangle class:

class Rectangle
{
  private int length;
  private int width;

  public void SetLength(int lengthParameter)
  {
    length = lengthParameter;
  }

  public int GetLength()
  {
    return length;
  }

  public void SetWidth(int widthParameter)
  {
    width = widthParameter;
  }

  public int GetWidth()
  {
    return width;
  }

  public int ComputeArea()
  {
    return length * width;
  }
}

Let’s look at each part of this code in order.

Using Our Class

using System;

class Program
{
  static void Main(string[] args)
  {
    Rectangle myRectangle = new Rectangle();
    myRectangle.SetLength(12);
    myRectangle.SetWidth(3);
    int area = myRectangle.ComputeArea();
    Console.WriteLine(
      "Your rectangle's length is "
        + $"{myRectangle.GetLength()}, and its width is "
        + $"{myRectangle.GetWidth()}, so its area is {area}."
    );
  }
}

Flow of Control with Objects

Accessing object members

Method calls in more detail

Methods and instance variables

Methods and return values

Introduction to UML

Variable Scope

Instance variables vs. local variables

Definition of scope

Variables with overlapping scopes

Constants

Reference Types: More Details

Constructors and Methods

Default Values and the ClassRoom Class

A class we will use for subsequent examples

Constructors

Writing a constructor

Methods with multiple parameters

Writing multiple constructors

Writing ToString Methods

Method Signatures and Overloading

Name uniqueness in C#

Method signatures

Calling overloaded methods

Constructors in UML

Properties

Introduction

Writing properties

Using properties

In More Details

Properties in UML Class Diagrams

Simple Notation

More Accurate Notation

In general, instead of writing for example

+ <<properties>> Explanation: string

one can write

+ <<get, set>> Explanation: string

or even

+ <<set>> Explanation: string
+ <<get>> Explanation: string

The benefit of this notation is that read-only properties can easily be integrated in the UML class diagram, by simply omitting the <<set>> line:

+ <<get>> Radius : decimal

The static Keyword

Static Methods

Different ways of calling methods

Declaring static methods

static methods and instances

Uses for static methods

Static Variables

Defining static variables

Using static variables

Static methods and variables

Summary of static access rules

Static Classes

Generic Type Parameter

Introduction

Imagine that you want to write a method that takes as an argument an array and returns an array of the same type, but with the values reversed. You may write the following code:

public class Helper{
    public static int[] Reverse(int[] arrayP)
    {
        int[] result = new int[arrayP.Length];
        int j = 0;
        for (int i = arrayP.Length - 1; i >= 0; i--)
        {
            result[j] = arrayP[i];
            j++;
        }
        return result;
    }
}

Then, this method could be used as follows:

int[] array1 = {0, 2, 3, 6};
int[] array1reversed = Helper.Reverse(array1);

And then array1reversed would contain 6, 3, 2, 0.

This method works as intended, but you can use it only with arrays of integers. If you want to use a similar method with arrays of, say, char, then you need to copy-and-paste the code above and to replace every occurrence of int by char. This is not very efficient, and it is error-prone.

Generic Types

There is a tool in C# to avoid having to be too specific, and to be able to tell the compiler that the method will work “with some type”, called generic type parameter, using the keyword T. In essence, <T> is affixed after the name of the method to signal that the method will additionally require to instantiate T with a particular type.

The previous method would become:

public class Helper{
    public static T[] Reverse<T>(T[] arrayP)
    {
        T[] result = new T[arrayP.Length];
        int j = 0;
        for (int i = arrayP.Length - 1; i >= 0; i--)
        {
            result[j] = arrayP[i];
            j++;
        }
        return result;
    }
}

where three occurrences of int[] were replaced by T[], and <T> was additionally added between the name of the method and its parameters. This method is used as follows:

int[] array1 = {0, 2, 3, 6};
int[] array1reversed = Helper.Reverse<int>(array1);

char[] array2 = {'a', 'b', 'c'};
char[] array2reversed = Helper.Reverse<char>(array2);

In essence, Reverse<int> tells C# that Reverse will be used with T being int (not int[], as the method uses T[] for its argument and return type). Note that to use the same method with char, we simply use Reverse<char>, and then we provide an array of char as parameters, and obtain an array of char in return.

Implicitly Typed Local Variables

Sometimes, the body of the method needs to declare variable with the same type as T. Indeed, imagine, for example, that we want to add to our Helper class a method that returns a string description of an array. We can write the following:

public static string Description(int[] arrayP)
{
    string returned = "";
    foreach (int element in arrayP)
    {
        returned += element + " ";
    }
    return returned;
}

but this method is specific to arrays of int, and we would have to write another one for char, for example. Making the header generic is “easy”, as we can use, as before:

public static string Description<T>(T[] arrayP)

but the body is problematic: what should be the type of the element variable in the header of the foreach? We cannot simply use T, but we can use implicitly typed variable. This technique, that uses the keyword var essentially tells C# to … figure out the type of the variable. In that case, since C# knows the type of the array you are passing, it can easily infer the type of its elements.

We can then rewrite the previous method as follows:

public static string Description<T>(T[] arrayP)
{
    string returned = "";
    foreach (var element in arrayP)
    {
        returned += element + " ";
    }
    return returned;
}

and use it with

Console.WriteLine(Helper.Display<char>(array2);

for example.

Inheritance

Motivation

Vehicle Example

Consider the following class:

A UML diagram for the Vehicle class (text version)

with the following implementation:

public class Vehicle
{
  public string Color { get; set; }
  private int numberOfWheels;

  public void SetNOW(int nowP)
  {
    if (nowP > 0)
      numberOfWheels = nowP;
    else
      numberOfWheels = -1;
  }

  public Vehicle()
  {
    Color = "undefined";
    numberOfWheels = -1;
  }

  public Vehicle(string cP, int nowP)
  {
    Color = cP;
    numberOfWheels = nowP;
  }

  public override string ToString()
  {
    return $"Number of wheels: {numberOfWheels}"
      + $"\nColor: {Color}";
  }
}

and say that we want to extend it to accommodate bikes. Bikes have, in addition to a color and a number of wheels, a fork length. Note that no other vehicle have a fork length, so it does not make sense to add this attribute to the Vehicle class.

A possible implementation is as follows:

public class Bike : Vehicle
{
  public double ForkLength;

  public Bike()
  {
    ForkLength = -1;
    SetNOW(2); // or base.setNOW(2);
  }

  public Bike(string cP, double flP)
    : base(cP, 2)
  {
    ForkLength = flP;
  }

  public override string ToString()
  {
    return base.ToString() + $"\nFork Length: {ForkLength}";
  }
}

Note:

The inheritance is represent in UML as follows:

A UML diagram for the Vehicle ⇽ Bike class (text version)

Observe that the ToString is indicated in the Bike class: this is an indication that the Vehicle’s ToString method is actually overriden in the Bike derived class.

Note that inheritance can be “chained”, as Bike could itself be the base class for a Bicycle class that could have e.g. a saddleType attribute (noting that a motorbike does not have a saddle, but a seat). We could then obtain a code as follows:

public class Bicycle : Bike
{
  private string saddleType;

  public Bicycle()
  {
    saddleType = "undefined";
  }

  public Bicycle(string cP, double flP, string sT)
    : base(cP, flP)
  {
    saddleType = sT;
  }

  public override string ToString()
  {
    return base.ToString() + $"\nSaddle Type: {saddleType}";
  }
}

Polymorphism

Motivation

Inheritance provides another very useful mechanism: (subtype) polymorphism. In a nutshell, the idea is that if a Pyramid9 class extends the Rectangle class, then a Pyramid object can still access all the Rectangle’s public methods, properties and attributes. Indeed, a Pyramid is a Rectangle: this is precisely what polymorphism means.

While the example below is abstract, it can be easily instantiated to e.g., a Cat class inheriting from a Pet class or a Pyramid class inheriting from a Rectangle class.

Inheriting Attributes, Properties and Methods

Consider the following two classes:

class Class1
{
  private string attribute1;

  public void SetAttribute1(string aP)
  {
    attribute1 = aP;
  }

  public string Property1 { get; set; }
}

class Class2 : Class1
{
  public string Property2 { get; set; }
}

Then,

This means that the following code is valid:

class Program
{
  static void Main()
  {
    Class1 object1 = new Class1();
    object1.SetAttribute1("Test");
    object1.Property1 = "Test";

    Class2 object2 = new Class2();
    object2.SetAttribute1("Test");
    object2.Property1 = "Test";
    object2.Property2 = "Test";
  }
}

Note, however, that object1.Property2 = "Test"; would not compile, since an object from Class1 cannot access the attributes, properties and methods of Class2. Stated differently, an object in Class2 is a(n object in) Class1, but the converse is not true: an object in Class1 is not an object in Class2.

Polymorphism and References

Note that a Class1 object can be created using a Class2 constructor, since an object in Class2 is a(n object in) Class1. Formally, we can write:

Class1 object3 = new Class2();

and then manipulate object3 like any other *object from Class1 (it is, in a way, “truncated”). In particular, we can use

object3.Property1 = "Test";

but object3.Property2 = "Test"; would not compile since we would be trying to access a property of Class2 with a Class1 object. Remember that an object in Class1 is not* an object in Class2, and that the way we declared it, object3 is a Class1 object.

Solving Ambiguity by Overriding

For Methods

Now, consider the following class implementation and usage:

class Class1
{
  public string Test()
  {
    return "Class1";
  }
}

class Class2 : Class1
{
  public string Test()
  {
    return "Class2";
  }
}
using System;

class Program
{
  static void Main()
  {
    Class1 object1 = new Class1();
    Console.WriteLine(object1.Test());

    Class2 object2 = new Class2();
    Console.WriteLine(object2.Test());
  }
}

Console.WriteLine(object1.Test()); will display “Class1”: there is no ambiguity, since object1 is a Class1 object, it can access only the methods in its class.

However, the situation is less clear for Console.WriteLine(object2.Test());: since object2 is “at the same time” a Class1 and a Class2 object, which method will be called? In this case, “Class2” will be displayed since C# prefers the “closest” method available (that is, the one in the same class as the calling object). However, a warning will be issued by the compiler because the Test method in Class2 “hides” the inherited method Test from Class1.

A much better code explicitly instructs C# to override Class1’s Test method with Class2’s Test method. However, this further requires Class1’s Test method to explicitly give permission to be overriden, using the virtual keyword:

class Class1
{
  public virtual string Test()
  {
    return "Class1";
  }
}

class Class2 : Class1
{
  public sealed override string Test()
  {
    return "Class2";
  }
}

class Class3 : Class2
{
  public override string Test()
  {
    return "Class 3";
  }
}

This program will also display, as expected,

Class1
Class2

but this time the compiler will not complain: there is no ambiguity, as Class2’s Test method must explicitly take precedence when an object in Class2 is calling a Test method.

Note that by default, methods are non-virtual, and non-virtual method cannot be overridden. However, overriding methods are treated as virtual and can be overridden themselves, unless they use the sealed keyword, as follows:

public override sealed string Test(){}

Such a method cannot be overridden by classes inheriting from the class to which they belong.

Last but not least, note that an override method must have the same signature as the overridden method.

For Attributes and Properties

Virtual attributes and properties can similarly be overridden, provided of course the overriding property or attribute has the same datatype and name as the virtual method or property. Consider for instance an int Property in a Class1 class with no requirement that is inherited by a Class2 that wish to forbid negative values. One could do the following:

class Class1
{
  public virtual int Property { get; set; }
}
using System;

class Class2 : Class1
{
  private int attribute;
  public override int Property
  {
    set
    {
      if (value < 0)
        throw new ArgumentOutOfRangeException();
      else
        attribute = value;
    }
    get { return attribute; }
  }
}

Note that the property in Class2 has a backing field while there is no need for it in Class1.

The following would then throw an exception when the object2.Property = -12; statement would be executed:

using System;

class Program
{
  static void Main()
  {
    Class1 object1 = new Class1();
    object1.Property = -12;

    Class2 object2 = new Class2();
    try
    {
      object2.Property = -12;
    }
    catch
    {
      Console.WriteLine(
        "In Class2, Property cannot be set to a negative value."
      );
    }
  }
}

Note that, as for methods, overriding properties are by default virtual and can be overridden, for example as follows:

class Class3 : Class2
{
  public override int Property { set; get; }
}

Abstract Classes

Motivation

Consider the following situation:

The mechanism used to obtain this behavior (being able to inherit from a class while disallowing instantiating it) is achieved using the abstract keyword.

Example

Consider a (shortened) version of the example above. We start by implementing an abstract Person class:

abstract class Person
{
  public string Name { get; set; }
  public abstract string Id { set; }
}

Note that the Id property is also marked as abstract: this means that the derived class will have to re-implement this property’s setter. Then, we can implement the Student and Employee classes by inheriting from the Person class:

using System;

class Student : Person
{
  private string major;
  public override string Id
  {
    set
    {
      if (value[0] != 'S')
        throw new ArgumentException(
          "A student ID must start with an 'S'."
        );
    }
  }
}
using System;

class Employee : Person
{
  private decimal hourlyPay;
  public override string Id
  {
    set
    {
      if (value[0] != 'E')
        throw new ArgumentException(
          "An employee ID must start with an 'E'."
        );
    }
  }
}

Using this code, the statement

Person test = new Person();

would return the error message “Cannot create an instance of the abstract type or interface ‘Person’”.

Furthermore, the following exemplifies the expected behavior:

using System;

class Program
{
  static void Main()
  {
    // Person test = new Person(); // Cannot create an instance of the abstract type or interface 'Person'
    Employee Harley = new Employee();
    Harley.Id = "E8190";

    Student Morgan = new Student();
    try
    {
      Morgan.Id = "E8194";
    }
    catch
    {
      Console.WriteLine(
        "We cannot set the Id of a student to a string not starting with 'S'!"
      );
    }
    Morgan.Id = "S8194";
  }
}

The statement Morgan.Id = "E8194"; will raise exception, but Morgan.Id = "S8194"; will execute without throwing an error.

Additional Details: Abstract Properties and Methods

UML Class Diagram Representation

A UML diagram for the Person ⇽ Student class (text version)

Interfaces

Motivation

Imagine you want to represent a variety of devices, and comes up with the following UML diagram:

A UML diagram for the ComputingDevice ⇽ Abacus class (text version)

Note that it is possible to gather that e.g., the Instructions() method in the Abacus class is overriding the Instructions() method in the ComputingDevice class because it has the same signature: this can be the case only because it is overriding the inherited abstract method.

Your abstract classes are “completely abstract”, in the sense that all of their properties and methods are abstract, but it serves your purpose just well:

A class that is “completely abstract” actually forces you to enforce a series of constraints and is a good way of making sure that you are consistent e.g., with the naming of your methods, the accessibility of your properties, or the return type of your methods.

You implement it as follows:

abstract class ComputingDevice
{
  public abstract double IPS { get; set; }
  public abstract void Instructions();
}
using System;

class Abacus : ComputingDevice
{
  private double ips;
  public override double IPS
  {
    get { return ips; }
    set
    {
      if (value < 0 || value > 1000)
        throw new ArgumentException(
          "This is not plausible"
        );
      else
        ips = value;
    }
  }
  public string Material { get; set; }

  public Abacus(double ipsP, string materialP)
  {
    IPS = ipsP;
    Material = materialP;
  }

  public override void Instructions()
  {
    Console.WriteLine(
      "Refer to https://www.wikihow.com/Use-an-Abacus"
    );
  }
}
abstract class ElectricalDevice
{
  public abstract int Voltage { get; set; }
  public abstract int Frequency { get; set; }
  public abstract void SafetyNotice();
}
using System;

class USWashingMachine : ElectricalDevice
{
  private int voltage;
  public override int Voltage
  {
    get { return voltage; }
    set
    {
      if (value < 110 || value > 220)
      {
        throw new ArgumentOutOfRangeException();
      }
      else
      {
        voltage = value;
      }
    }
  }
  private int frequency;
  public override int Frequency
  {
    get { return frequency; }
    set
    {
      if (value != 50 && value != 60)
      {
        throw new ArgumentOutOfRangeException();
      }
      else
        frequency = value;
    }
  }

  public USWashingMachine(int vP, int fP)
  {
    Voltage = vP;
    Frequency = fP;
  }

  public override void SafetyNotice()
  {
    Console.WriteLine(
      "Refer to https://www.energy.gov/sites/"
        + "prod/files/2016/06/f32/"
        + "NFPA_DryerWasherSafetyTips.pdf"
    );
  }
}
using System;

class Program
{
  static void Main()
  {
    Abacus test0 = new Abacus(1.5, "Wood");
    test0.Instructions();
    USWashingMachine test1 = new USWashingMachine(120, 50);
    test1.SafetyNotice();
  }
}
(Download this code)

Then, you would like to add a “Computer” class, but face an issue: classes can inherit only from one class directly, but of course a computer is both an electrical device and a computing device. A solution is to switch to interfaces.

Explanations

Interfaces are completely abstract classes: they do not implement anything, they simply force classes inheriting from them (we actually say that realizes them) to implement certain features.

In Diagram

Interfaces are prefixed by the «Interface» mention, and have all their properties and methods marked as abstract (so, in italics). A class can “inherits” from multiple interface (we say that it realizes multiple interfaces), and this is marked with an arrow with an open triangle end and a dashed line10.

A UML diagram for the ComputingDevice ◁┈ Computer class (text version)

An Implementation

Implementing such interfaces and their realization could be done as follows:

interface ComputingDevice
{
  double IPS { get; set; }
  void Instructions();
}
interface ElectricalDevice
{
  int Voltage { get; set; }
  int Frequency { get; set; }
  void SafetyNotice();
}
using System;
class Computer: ElectricalDevice, ComputingDevice
{
    private double ips;
    public double IPS
    {
        get { return ips; }
        set
        {
            if (value < 0)
                throw new ArgumentException(
                  "This is not possible."
                );
            else
                ips = value;
        }

    }
    private int voltage;
    public int Voltage
    {
        get { return voltage; }
        set
        {
            if (value < 110 || value > 220)
            {
                throw new ArgumentOutOfRangeException();
            }
            else
            {
                voltage = value;
            }
        }
    }
    private int frequency;
    public int Frequency
    {
        get { return frequency; }
        set
        {
            if (value != 50 && value != 60)
            {
                throw new ArgumentOutOfRangeException();
            }
            else
                frequency = value;
        }
    }
    public Computer(double ipsP, int voltageP, int frequencyP)
    {
        IPS = ipsP;
        Voltage = voltageP;
        Frequency = frequencyP;
    }
    public void Instructions()
    {
        Console.WriteLine(
          "Refer to your operating system manual."
        );
    }
    public void SafetyNotice()
    {
        Console.WriteLine("Refer to your manufacturer website.");
    }
}
using System;

class Program
{
    static void Main()
    {
        Computer test0 = new Computer(100000, 120, 50);
        test0.SafetyNotice();
        test0.Instructions();
    }
}
(Download this code)

Note that

A More Complicated Example

A UML diagram for the IAnimation ◁┈ Shape class (text version)

in this archive

Introduction

Decisions are a constant occurrence in daily life. For instance consider an instructor teaching CSCI 1301. At the beginning of class the instructor may

This type of “branching” between multiple choices can be represented with an activity diagram:

“An Activity Diagram on Teaching a Class”

In C#, we will express

Both structures need a datatype to express the result of a decision (“Is it true that there are questions.”, or “Is it false that there is a quiz.”) called Booleans. Boolean values can be set with conditions, that can be composed in different ways using three operators (“and”, “or” and “not”). For example, “If today is a Monday or Wednesday, and it is not past 10:10 am, the class will also include a brief reminder about the upcoming exam.”

Booleans

Variables

We can store if something is true or false (“The user has reached the age of majority”, “The switch is on”, “The user is using Windows”, “This computer’s clock indicates that we are in the afternoon”, …) in a variable of type boolean, which is also known as a boolean flag. Note that true and false are the only possible two values for boolean variables: there is no third option!

We can declare, assign, initialize and display a boolean variable (flag) as with any other variable:

bool learning_how_to_program = true;
Console.WriteLine(learning_how_to_program);

Operations on Boolean Values

Boolean variables have only two possible values (true and false), but we can use three operations to construct more complex booleans:

  1. “and” (&&, conjunction),
  2. “or” (||, disjunction),
  3. “not” (!, negation).

Each has the precise meaning described here:

  1. the condition “A and B” is true if and only if A is true, and B is true,
  2. “A or B” is false if and only if A is false, and B is false (that is, it takes only one to make their disjunction true),
  3. “not A” is true if and only if A is false (that is, “not” “flips” the value it is applied to).

The expected results of these operations can be displayed in truth tables, as follows:

Operation Value
true && true true
true && false false
false && true false
false && false false
Operation Value
true || true true
true || false true
false || true true
false || false false
Operation Value
!true false
!false true

These tables can also be written in 2-dimensions, as can be seen for conjunction on wikipedia.

Equality and Relational Operators

Boolean values can also be set through expressions, or tests, that “evaluate” a condition or series of conditions as true or false. For instance, you can write an expression meaning “variable myAge has the value 12” which will evaluate to true if the value of myAge is indeed 12, and to false otherwise. To ease your understanding, we will write “expression true” to indicate that “expression” evaluates to true below, but this is not part of C#’s syntax.

Here we use two kinds of operators:

Relational operators will be primarily used for numerical values.

Equality Operators

In C#, we can test for equality and inequality using two operators, == and !=.

Mathematical Notation C# Notation Example
= == 3 == 4 false
!= 3!=4 true

Note that testing for equality uses two equal signs: C# already uses a single equal sign for assignments (e.g. myAge = 12;), so it had to pick another notation! It is fairly common across programing languages to use a single equal sign for assignments and double equal for comparisons.

Writing a != b (“a is not the same as b”) is actually logically equivalent to writing !(a == b) (“it is not true that a is the same as b”), and both expressions are acceptable in C#.

We can test numerical values for equality, but actually any datatype can use those operators. Here are some examples for int, string, char and bool:

int myAge = 12;
string myName = "Thomas";
char myInitial = 'T';
bool cs_major = true;
Console.WriteLine("My age is 12: " + (myAge == 12));
Console.WriteLine("My name is Bob: " + (myName == "Bob"));
Console.WriteLine("My initial is Q: " + (myInitial == 'Q'));
Console.WriteLine("My major is Computer Science: " + cs_major);

This program will display

My age is 12: True
My name is Bob: False
My initial is Q: False
My major is Computer Science: True

Remember that C# is case-sensitive, and that applies to the equality operators as well: for C#, the string Thomas is not the same as the string thomas. This also holds for characters like a versus A.

Console.WriteLine("C# is case-sensitive for string comparison: " + ("thomas" != "Thomas"));
Console.WriteLine("C# is case-sensitive for character comparison: " + ('C' != 'c'));
Console.WriteLine("But C# does not care about 0 decimal values: " + (12.00 == 12));

This program will display:

C# is case-sensitive for string comparison: True
C# is case-sensitive for character comparison: True
But C# does not care about 0 decimal values: True

Relational Operators

We can test if a value or a variable is greater than another, using the following relational operators.

Mathematical Notation C# Notation Example
> > 3 > 4 false
< < 3 < 4 true
or >= 3 >= 4 false
or <= 3 <= 4 true

Relational operators can also compare char, but the order is a bit complex (you can find it explained, for instance, in this stack overflow answer).

Precedence of Operators

All of the operators have a “precedence”, which is the order in which they are evaluated. The precedence is as follows:

Operator
! is evaluated before
*, /, and % which are evaluated before
+ and - which are evaluated before
<, >, <=, and >= which are evaluated before
== and != which are evaluated before
&& which is evaluated before
|| which comes last.
The `!' operator cannot be applied to operand of type `int'

Since ! has a higher precedence than ==, C# first attempts to compute the result of !4, which corresponds to “not 4”. As negation (!) is an operation that can be applied only to booleans, this expression does not make sense and C# reports an error. The expression can be rewritten to change the order of evaluation by using parentheses, e.g. you can write !(4 == 2), which will correctly be evaluated to true.

if

if Statements

Introduction

Example code with an if statement

Console.WriteLine("Enter your age");
int age = int.Parse(Console.ReadLine());
if (age >= 18)
{
    Console.WriteLine("You can vote!");
}
Console.WriteLine("Goodbye");

Syntax and rules for if statements

if-else Statements

Example:

if(age >= 18)
{
    Console.WriteLine("You can vote!");
}
else
{
    Console.WriteLine("You are too young to vote");
}
Console.WriteLine("Goodbye");

Syntax and comparison

Nested if-else Statements

Using nested if statements

if-else-if Statements

If-else-if syntax

Using if-else-if to solve the “floors problem”

if-else-if with different conditions

if-else-if vs. nested if

Switch

Switch Statements

Multiple equality comparisons

Syntax for switch statements

Example switch statement

switch with multiple statements

Intentionally omitting break

Scope and switch

Limitations of switch

While Loops

Introduction to while loops

Example code with a while loop

int counter = 0;
while(counter <= 3)
{
    Console.WriteLine("Hello again!");
    Console.WriteLine(counter);
    counter++;
}
Console.WriteLine("Done");

Syntax and rules for while loops

While loops may execute zero times

Ensuring the loop ends

Principles of writing a while loop

While Loop With Complex Conditions

In the following example, a complex boolean expression is used in the while statement. The program gets a value and tries to parse it as an integer. If the value can not be converted to an integer, the program tries again, but not more than three times.

int c;
string message;
int count;
bool res;

Console.WriteLine("Please enter an integer.");
message = Console.ReadLine();
res = int.TryParse(message, out c);
count = 0; // The user has 3 tries: count will be 0, 1, 2, and then we default.
while (!res && count < 3)
{
    count++;
    if (count == 3)
    {
        c = 1;
        Console.WriteLine("I'm using the default value 1.");
    }
    else
    {
        Console.WriteLine("The value entered was not an integer.");
        Console.WriteLine("Please enter an integer.");
        message = Console.ReadLine();
        res = int.TryParse(message, out c);
    }
}
Console.WriteLine("The value is: " + c);

do while

Comparing while and if statements

if(number < 3)
{
    Console.WriteLine("Hello!");
    Console.WriteLine(number);
    number++;
}
Console.WriteLine("Done");

and

while(number < 3)
{
    Console.WriteLine("Hello!");
    Console.WriteLine(number);
    number++;
}
Console.WriteLine("Done");

Code duplication in while loops

Console.WriteLine("Enter the item's price.");
decimal price = decimal.Parse(Console.ReadLine());
while(price < 0)
{
    Console.WriteLine("Invalid price. Please enter a non-negative price.");
    price = decimal.Parse(Console.ReadLine());
}
Item myItem = new Item(desc, price);

Introduction to do-while

decimal price;
do
{
    Console.WriteLine("Please enter a non-negative price.");
    price = decimal.Parse(Console.ReadLine());
} while(price < 0);
Item myItem = new Item(desc, price);

Formal syntax and details of do-while

do
{
    <statements>
} while(<condition>);

do-while loops with multiple conditions

decimal price;
bool parseSuccess;
do
{
    Console.WriteLine("Please enter a price (must be non-negative).");
    parseSuccess = decimal.TryParse(Console.ReadLine(), out price);
} while(!parseSuccess || price < 0);
Item myItem = new Item(desc, price);

Input Validation

Valid and invalid data

class Item
{
  private string description;
  private decimal price;

  public Item(string initDesc, decimal initPrice)
  {
    description = initDesc;
    price = initPrice;
  }

  public decimal GetPrice()
  {
    return price;
  }

  public void SetPrice(decimal p)
  {
    price = p;
  }

  public string GetDescription()
  {
    return description;
  }

  public void SetDescription(string desc)
  {
    description = desc;
  }
}
Console.WriteLine("Enter the item's description");
string desc = Console.ReadLine();
Console.WriteLine("Enter the item's price (must be positive)");
decimal price = decimal.Parse(Console.ReadLine());
Item myItem = new Item(desc, price);

In this code, if the user enters a negative number, the myItem object will have a negative price, even though that does not make sense.

decimal price = decimal.Parse(Console.ReadLine());
Item myItem = new Item(desc, (price >= 0) ? price : 0);

In this code, the second argument to the Item constructor is the result of the conditional operator, which will be 0 if price is negative.

public Item(string initDesc, decimal initPrice)
{
    description = initDesc;
    price = (initPrice >= 0) ? initPrice : 0;
}

then the instantiation new Item(desc, price) would never be able to create an object with a negative price. If the user provides an invalid price, the constructor will ignore their value and initialize the price instance variable to 0 instead.

Ensuring data is valid with a loop

Console.WriteLine("Enter the item's price.");
decimal price = decimal.Parse(Console.ReadLine());
while(price < 0)
{
    Console.WriteLine("Invalid price. Please enter a non-negative price.");
    price = decimal.Parse(Console.ReadLine());
}
Item myItem = new Item(desc, price);

Ensuring the user enters a number with TryParse

Console.WriteLine("Guess a number"):
int guess = int.Parse(Console.ReadLine());
if(guess == favoriteNumber)
{
    Console.WriteLine("That's my favorite number!");
}
string userInput = Console.ReadLine();
int intVar;
bool success = int.TryParse(userInput, out intVar);
bool success = <numeric datatype>.TryParse(<string to convert>, out <numeric variable to store result>)

Note that the variable you use in the out parameter must be the same type as the one whose TryParse method is being called. If you write decimal.TryParse, the out parameter must be a decimal variable.

Console.WriteLine("Please enter an integer");
string userInput = Console.ReadLine();
int intVar;
bool success = int.TryParse(userInput, out intVar);
if(success)
{
    Console.WriteLine($"The value entered was an integer: {intVar}");
}
else
{
    Console.WriteLine($"\"{userInput}\" was not an integer");
}
Console.WriteLine(intVar);
bool success = int.TryParse(Console.ReadLine(), out int intVar);
Console.WriteLine("Please enter an integer");
bool success = int.TryParse(Console.ReadLine(), out int number);
while(!success)
{
    Console.WriteLine("That was not an integer, please try again.");
    success = int.TryParse(Console.ReadLine(), out number);
}

The foreach Loop

For Loops

Counter-controlled loops

for loop example and syntax

Limitations and Pitfalls of Using for Loops

Scope of the for loop’s variable

Accidentally re-declaring a variable

Accidentally double-incrementing the counter

More Ways to use for Loops

Complex condition statements

Complex update statements

Complex loop bodies

Combining for and while loops

Loop Vocabulary

Variables and values can have multiple roles, but it is useful to mention three different roles in the context of loops:

Counter

Variable that is incremented every time a given event occurs.

int i = 0; // i is a counter
while (i < 10){
    Console.WriteLine($"{i}");
    i++;
}
Sentinel Value

A special value that signals that the loop needs to end.

Console.WriteLine("Give me a string.");
string ans = Console.ReadLine();
while (ans != "Quit") // The sentinel value is "Quit".
{
    Console.WriteLine("Hi!");
    Console.WriteLine("Enter \"Quit\" to quit, or anything else to continue.");
    ans = Console.ReadLine();
}
Accumulator

Variable used to keep the total of several values.

int i = 0, total = 0;
while (i < 10){
    total += i; // total is the accumulator.
    i++;
}

Console.WriteLine($"The sum from 0 to {i} is {total}.");

We can have an accumulator and a sentinel value at the same time:

Console.WriteLine("Enter a number to sum, or \"Done\" to stop and print the total.");
string enter = Console.ReadLine();
int sum = 0;
while (enter != "Done")
{
    sum += int.Parse(enter);
    Console.WriteLine("Enter a number to sum, or \"Done\" to stop and print the total.");
    enter = Console.ReadLine();
}
Console.WriteLine($"Your total is {sum}.");

You can have counter, accumulator and sentinel values at the same time:

int a = 0;
int sum = 0;
int counter = 0;
Console.WriteLine("Enter an integer, or N to quit.");
string entered = Console.ReadLine();
while (entered != "N") // Sentinel value
{
    a = int.Parse(entered);
    sum += a; // Accumulator
    Console.WriteLine("Enter an integer, or N to quit.");
    entered = Console.ReadLine();
    counter++; // counter
}
Console.WriteLine($"The average is {sum / (double)counter}");

We can distinguish between three “flavors” of loops (that are not mutually exclusive):

Sentinel controlled loop

The exit condition tests if a variable has (or is different from) a specific value.

User controlled loop

The number of iterations depends on the user.

Count controlled loop

The number of iterations depends on a counter.

Note that a user-controlled loop can be sentinel-controlled (that is the example we just saw), but also count-controlled (“Give me a value, and I will iterate a task that many times”).

Combining Classes and Decision Structures

Now that we have learned about decision structures, we can revisit classes and methods. Decision structures can make our methods more flexible, useful, and functional.

Using if Statements with Methods

There are several ways we can use if-else and if-else-if statements with methods:

Setters with Input Validation

Constructors with Input Validation

Boolean Parameters

Ordinary Methods Using if

Boolean Instance Variables

Using while Loops with Classes

There are several ways that while loops are useful when working with classes and methods:

Input Validation with Objects

Using Loops Inside Methods

Using Methods to Control Loops

Examples

The Room Class

The class and its associated Main method presented in this archive show how you can use classes, methods, constructors and decision structures all in the same program. It also exemplifies how a method can take an object as a parameter with InSameBuilding.

The corresponding UML diagram is:

A UML diagram for the Room class (text version)

The Loan Class

Similarly, this class and its associated Main method show how you can use classes, methods, constructors, decision structures, and user input validation all in the same program. This lab asks you to add the user input validation code, and you can download the following code in this archive.

/*
 * Application program for the "Loan" class.
 * This program gathers from the user all the information needed
 * to create a "proper" Loan object.
*/

using System;

class Program
{
  static void Main()
  {
    Console.WriteLine("What is your name?");
    string name = Console.ReadLine();

    Console.WriteLine(
      "Do you want a loan for an Auto (A, a), a House (H, h), or for some Other (O, o) reason?"
    );
    char type = Console.ReadKey().KeyChar; // This part of the code reads *a char* from the user.
    // We haven't studied it, but it's pretty straightforward.
    Console.WriteLine();

    /*
     * The part of the code that follows
     * does the convertion from the character
     * to the corresponding string.
     * We could have a method in the Loan
     * class that does it for us, but
     * we'll just do it "by hand" here
     * for simplicity.
     */
    string typeOfLoan;

    if (type == 'A' || type == 'a')
    {
      type = 'a';
      typeOfLoan = "an auto";
    }
    else if (type == 'H' || type == 'h')
    {
      type = 'h';
      typeOfLoan = "a house";
    }
    else
    {
      type = 'o';
      typeOfLoan = "some other reason";
    }

    // We display the information back to the user, and ask the next question:
    Console.WriteLine(
      $"{name}, you need money for {typeOfLoan}, great.\nWhat is your current credit score?"
    );
    int cscore = int.Parse(Console.ReadLine());

    Console.WriteLine("How much do you need, total?");
    decimal need = decimal.Parse(Console.ReadLine());

    Console.WriteLine("What is your down payment?");
    decimal down = decimal.Parse(Console.ReadLine());

    Loan myLoan = new Loan(name, type, cscore, need, down);
    Console.WriteLine(myLoan);
  }
}
/*
 * "Loan" class.
 * This class helps primarily in computing
 * an APR based on information provided from the user.
 * A ToString method is provided.
 */
using System;

class Loan
{
  private string name; // For the name of the loan holder.
  private char type; // For the type ('a'uto, 'h'ouse or 'o'ther) of the loan
  private int cscore; // For the credit score.
  private decimal amount; // For the amount of money loaned.
  private decimal rate; // For the A.P.R., the interest rate.

  /*
   * Our constuctor will compute the amount and the rate
   * based on the information given as arguments.
   * The name, type and credit score will simply be given as arguments.
   */
  public Loan(
    string nameP,
    char typeP,
    int cscoreP,
    decimal needP,
    decimal downP
  )
  {
    name = nameP;
    type = typeP;
    cscore = cscoreP;
    if (cscore < 421)
    {
      Console.WriteLine(
        "Sorry, we can't accept your application."
      );
      amount = -1;
      rate = -1;
    }
    else
    {
      amount = needP - downP;

      switch (type)
      {
        case ('a'):
          rate = .05M;
          break;

        case ('h'):
          if (cscore > 600 && amount < 1000000M)
            rate = .03M;
          else
            rate = .04M;
          break;

        case ('o'):
          if (cscore > 650 || amount < 10000M)
            rate = .07M;
          else
            rate = .09M;
          break;
      }
    }
  }

  public override string ToString()
  {
    string typeName = "";
    switch (type)
    {
      case ('a'):
        typeName = "an auto";
        break;

      case ('h'):
        typeName = "a house";
        break;
      case ('o'):
        typeName = "another reason";
        break;
    }
    return "Dear "
      + name
      + $", you borrowed {amount:C} at {rate:P} for "
      + typeName
      + ".";
  }
}

Break and continue

Conditional iteration

int sum = 0;
for(int i = 0; i < myArray.Length; i++)
{
    if(myArray[i] % 2 == 0)
    {
        Console.WriteLine(myArray[i]);
        sum += myArray[i];
    }
}

Since the entire body of the for loop is contained within an if statement, the iterations where myArray[i] is odd will skip the body and do nothing.

Skipping iterations with continue

int sum = 0;
for(int i = 0; i < myArray.Length; i++)
{
    if(myArray[i] % 2 != 0)
        continue;
    Console.WriteLine(myArray[i]);
    sum += myArray[i];
}

If myArray[i] is odd, the computer will execute the continue statement and immediately start the next iteration of the loop. This means that the rest of the loop body (the other two statements) only gets executed if myArray[i] is even.

Loops with multiple end conditions

int sum = 0, userNum = 0;
bool success = true;
while(success && userNum >= 0)
{
    sum += userNum;
    Console.WriteLine("Enter a positive number to add it. "
    + "Enter anything else to stop.");
    success = int.TryParse(Console.ReadLine(), out userNum);
}
Console.WriteLine($"The sum of your numbers is {sum}");

Ending the loop with break

int sum = 0, userNum = 0;
while(userNum >= 0)
{
    sum += userNum;
    Console.WriteLine("Enter a positive number to add it. "
    + "Enter anything else to stop.");
    if(!int.TryParse(Console.ReadLine(), out userNum))
        break;
}
Console.WriteLine($"The sum of your numbers is {sum}");
int product = 1;
for(int i = 0; i < myArray.Length; i++)
{
    if(myArray[i] == 0)
        break;
    product *= myArray[i];
}
int product = 1;
foreach(int number in myArray)
{
    if(number == 0)
        break;
    product *= number;
}

The Conditional Operator

string output;
if(myInt % 2 == 0)
{
    output = "Even";
}
else
{
    output = "Odd";
}

Assignment with the conditional operator

string output = (myInt % 2 == 0) ? "Even" : "Odd";

When this line of code is executed:

condition ? true_expression : false_expression;

Conditional operator examples

int answer = (myInt % 2 == 0) ? myInt / 2 : myInt + 1;

If myInt is even, the computer will evaluate myInt / 2 and assign the result to answer. If it is odd, the computer will evaluate myInt + 1 and assign the result to answer.

Console.WriteLine("What is your height in meters?");
double userHeight = double.Parse(Console.ReadLine());
double height = (userHeight >= 0.0) ? userHeight : 0.0;
bool isAdult = age >= 18;
decimal price = isAdult ? 5.0m : 2.5m;
string closingTime = isAdult ? "10:00 pm" : "8:00 pm";

Recursion

The code for this lecture is available in this archive (first parts) and this one (listing files and folders recursively).

Introduction

Recursion is a central notion in programming, simple to state but difficult to master: a method is recursive if it calls itself. This concept is related to the idea of repetition, or looping, of program parts, and come with the same danger of not terminating. Below, we present some simple recursive programs: while some could be written without recursion, some would be very hard, if possible at all, to write without using recursion.

First Examples

Consider the following:

  static void displayAll(int n)
  {
    if (n > 0)
    {
      Console.Write($"{n} ");
      displayAll(n - 1);
    }
  }

If we call displayAll(3);, then the following will happen:

  1. displayAll(3) will test that 3>0,
  2. displayAll(3) will display “3 ”,
  3. displayAll(3) will call displayAll(2),
    1. displayAll(2) will test that 2>0,
    2. displayAll(2) will display “2 ”,
    3. displayAll(2) will call displayAll(1),
      1. displayAll(1) will test that 1>0,
      2. displayAll(1) will display “1 ”,
      3. displayAll(1) will call displayAll(0),
        1. displayAll(0) will test that 0>0,
        2. displayAll(0) will terminate.
      4. displayAll(1) will terminate.
    4. displayAll(2) will terminate.
  4. displayAll(3) will terminate.

Hence, displayAll calls itself with a smaller number, unless that number is 0, in which case it simply terminates. In our example, it would display “3 2 1 ”.

When the function calls itself matters a lot. Indeed, consider displayRAll, which calls itself before executing the Console.WriteLine instruction:

  static void displayRAll(int n)
  {
    if (n > 0)
    {
      displayRAll(n - 1);
      Console.Write($"{n} ");
    }
  }

If we call displayRall(3);, then the following will happen:

  1. displayRall(3) will test that 3>0,
  2. displayRall(3) will call displayRall(2),
    1. displayRall(2) will test that 2>0,
    2. displayRall(2) will call displayRall(1),
      1. displayRall(1) will test that 1>0,
      2. displayRall(1) will call displayRall(0),
        1. displayRall(0) will test that 0>0,
        2. displayRall(0) will terminate.
      3. displayRall(1) will display “1 ”,
      4. displayRall(1) will terminate.
    3. displayRall(2) will display “2 ”,
    4. displayRall(2) will terminate.
  3. displayRall(3) will display “3 ”,
  4. displayRall(3) will terminate.

In this example, “1 2 3 ” would be displayed: the order is reversed with respect to displayAll!

❗ Caution
Recursion can be very powerful and can very easily make your program crash or misbehave. To see it for yourself, after saving all important documents, replace - with + in the previous examples and run the programs again.

displayAll is an example of tail recursion: the recursive call is the last statement in the method. displayRAll is an example of head recursion: the recursive call is the first statement in the method. They are furthormore both examples of linear recursion, as they call themselves only once.

Recursive Methods Returning a Value

Recursive methods can also return a value, used by previous calls to compute some other value.

Multiplication

For example, consider that multiplication can be defined by addition: indeed, x × y is y + y + y + … + y where y is summed x times. Stated differently (read: recursively), x × y is y + ((x − 1) × y). We can implement such a program easily:

  static int mult(int x, int y)
  {
    if (x == 0)
    {
      return 0;
    }
    else if (x == 1)
    {
      return y;
    }
    else
    {
      return y + mult(x - 1, y);
    }
  }

For example, mult(2, 10) tests that 2 is neither 0 nor 1, and adds 10 with the result of mult(1, 10), which is 10 since the first argument is 1.

Observe that mult(10000000, 0) would call mult 10000001 times and add 0 to itself 10000001 times: this algorithm is not very efficient!

Factorial

The factorial of n is n! = n × (n − 1) × (n − 2) × (n − 3) × … × 1. This function can easily be implemented using recursion:

  static int factorial(int n)
  {
    if (n == 0)
      return 1;
    else
      return (factorial(n - 1) * n);
  }

Note that this code actually compute e.g., 5! = 5 × 4 × 3 × 2 × 1 × 1 (with one superfluous ×1): can you see why?

Listing Files and Directories – Recursively

While multiplication and factorial can be implemented without recursion, some structures makes it natural, or even required, to use recursion. Going through folders and files is an example of such situation.

using System;
using System.IO;

class Program
{
  static void Main()
  {
    // We first locate where we currently are.
    DirectoryInfo currentDir = new DirectoryInfo(
      Directory.GetCurrentDirectory()
    );
    Console.WriteLine("Starting from " + currentDir + ".");
    int count = 5;
    // We go up 5 folders or until we reach the
    // root folder, whichever comes first.
    while (currentDir.Parent != null && count > 0)
    {
      currentDir = currentDir.Parent;
      count--;
      Console.WriteLine("Going up to " + currentDir + ".");
    }
    Console.WriteLine(
      "Now listing files and folders from here:"
    );
    ListDir(currentDir.ToString());
  }

  // Code in part inspired from
  // https://stackoverflow.com/a/929277
  static void ListDir(string sourceDir)
  {
    try
    {
      Console.WriteLine(sourceDir);

      foreach (string file in Directory.GetFiles(sourceDir))
        Console.WriteLine(file);

      foreach (
        string directory in Directory.GetDirectories(
          sourceDir
        )
      )
        ListDir(directory);
    }
    catch (Exception e)
    {
      Console.WriteLine(e.Message);
    }
  }
}

Note that our previous examples were calling themselves only once per method call, but that ListDir calls itself as many times as there are folders in the folder currently examined.

More on Recursion

The code for this lecture is available in this archive.

Re-Introduction

We previously defined recursion as follows:

a method is recursive if it calls itself.

Applied very strictly, the simplest (and most likely shortest) recursive method is the following:

    // Warning: dangerous function!
    void R()
    {
      R();
    }

It is a method (R) that simply … calls itself. Even if this method does not “do” anything, calling it will most likely make your program crash, since R will keep calling itself forever: this is actually an example of an infinite loop, and the basics of the “fork bomb” attack11.

A better definition of recursion would include something about the method eventually terminating, like the following:

    void CountDown(int n)
    {
      if (n == 0)
      {
        Console.WriteLine($"{n}: Blast off!");
      }
      else
      {
        Console.Write($"{n}…");
        CountDown(n - 1);
      }
    }

In that case, if we call e.g., CountDown(10), then would be displayed:

10…9…8…7…6…5…4…3…2…1…0: Blast off!

But note that this method is not always terminating: indeed, calling CountDown(-1) actually loops forever, since removing 1 to -1 repetitively will never make it reach 0 (if we forget about overflows for an instant).

A possible way to patch this would be to have two additional method: one to count “up” to 0, and one that decides which method to call:

    void CountUp(int n)
    {
      if (n == 0)
      {
        Console.WriteLine($"{n}: Blast off!");
      }
      else
      {
        Console.Write($"{n}…");
        CountUp(n + 1); // <- Only change.
      }
    }
    void Count(int n)
    {
      if (n < 0)
        CountUp(n);
      else
        CountDown(n);
    }
    Count(10);
    Count(-10);

As we can see, Count itself is not recursive, but it calls a recursive method.

Finally, methods can be mutually recursive: a method MyTurn can call a YourTurn method that itself calls MyTurn. While neither method are recursive, they create a recursive situation, as exemplified below:

    void MyTurn(int n)
    {
      if (n < 0)
      {
        Console.WriteLine("The Game is over.");
      }
      else
      {
        Console.WriteLine("It's my turn");
        n--;
        if (n < 0)
        {
          Console.WriteLine("The Game is over.");
        }
        else
        {
          YourTurn(n);
        }
      }
    }
    void YourTurn(int n)
    {
      Console.WriteLine("It's your turn.");
      MyTurn(n);
    }

Note that determining how many time both methods will be executed may not be easy: in our example, if MyTurn(4) is called, can you determine what will be displayed?

Arrays and Recursion

Any structure over which we can iterate can be treated using recursion, and arrays are no exception. In the following, we will re-implement two simple methods using recursion: one to decide if an array is sorted, and one that implements binary search.

Sorted Array Using Recursion

Given an array and a current index, to determine if the array is sorted, one can:

Note that our definition above is recursive: being sorted is defined using being sorted.

Assuming the array is sorted up to currentIndex, the following will return true if the rest of the array is sorted, false otherwise:

    bool SortedH(int[] aP, int currentIndex)
    {
      if (aP.Length == currentIndex + 1)
        return true;
      else if (aP[currentIndex] > aP[currentIndex + 1])
        return false;
      else
        return SortedH(aP, currentIndex + 1);
    }

The first test check if we are done (in which case the array is sorted), the second compare the value at the current index with the one that follows, and the last one kicks in the reduction by stipulating that if the other two tests failed, then the array is sorted if the rest of the array is.

Compared to our informal above, we are missing the “making sure the left of the current index is sorted” bit, unless we start with current index … 0! Putting it all together, we can define Sorted calling the recursive SortedH method with the right arguments (and after performing some checks):

    bool Sorted(int[] aP)
    {
      if (aP == null)
        return false;
      else
        return SortedH(aP, 0);
    }

Binary Search Using Recursion

We can perform binary search using recursion:

    bool BinFindH(int[] aP, int start, int end, int target)
    {
      int mid = (start + end) / 2;
      if (start > end)
      {
        return false;
      }
      else
      {
        if (target == aP[mid])
        {
          return true;
        }
        else if (target > aP[mid])
        {
          return BinFindH(aP, mid + 1, end, target);
        }
        else
        {
          return BinFindH(aP, start, mid - 1, target);
        }
      }
    }
    // Binary search
    bool BinFind(int[] aP, int target)
    {
      return BinFindH(aP, 0, aP.Length - 1, target);
    }

Lists and Recursion

Lists are also naturally manipulated by recursive methods. We show, as an example, two ways of defining a method that construct a string describing a .NET list.

    string DisplayH(
      string retString,
      List<string> listP,
      int indexP
    )
    {
      if (listP.Count == indexP + 1)
      {
        return retString + listP[indexP] + ".\n";
      }
      else
      {
        retString += listP[indexP] + " -> ";
        return DisplayH(retString, listP, indexP + 1);
      }
    }

    string Display(List<string> ListP)
    {
      string retString = "";
      return DisplayH(retString, ListP, 0);
    }

Note that the DisplayH method is a bit cumbersome, as it must carry around

  1. The whole list (listP),
  2. The string that is being constructed (retString),
  3. An index (indexP).

An alternative way of writing such a method is to

  1. shorten the list as we go (using RemoveAt),
  2. and to use a reference to the string,

as follows:

    void DisplayRef(ref string descP, List<string> listP)
    {
      if (listP == null || listP.Count == 0)
      {
        descP += ".\n";
      }
      else if (listP.Count == 1)
      {
        descP += listP[0] + ".\n";
      }
      else
      {
        descP += listP[0] + " -> ";
        listP.RemoveAt(0);
        DisplayRef(ref descP, listP);
      }
    }

But note that the list is actually shortened by the RemoveAt instruction: if we additionally have to leave the original string unmodified, then a copy of the list must be created, using e.g.

    List<string> listCopy = new List<string>(
      operatingSystems
    );

Introduction

Arrays are structures that allow you to store multiple values in memory using a single name and indexes. Internally, an array contains a fixed number of variables (called elements) of a particular type12. The elements in an array are always stored in a contiguous block of memory, providing fast and efficient access.

An array can be:

Multidimensional arrays can be

Arrays are useful, for instance,

Single-Dimensional Arrays

Introduction

You can define a single-dimensional array as follow:

<type>[] arrayName;

where

Before using an array, you must specify the number of elements in the array as follows:

arrayName = new <type>[<number of elements>];

where <type> is a type as before, and <number of elements>, called the size declarator, is a strictly positive integer which will correspond to the size of the array.

// Assigns <value> to the <index> element of the array arrayName.
arrayName[<index>] = <value>; 

// Display the <index> element of the array arrayName.
Console.WriteLine(arrayName[<index>]);

The index of the first element in an array is always zero; the index of the second element is one, and the index of the last element is the size of the array minus one. As a consequence, if you specify an index greater or equal to the number of elements, a run-time error will happen.

Indexing starting from 0 may seem surprising and counter-intuitive, but this is a largely respected convention across programing languages and computer scientists. Some insights on the reasons behind this (collective) choice can be found in this answer on Computer Science Educators.

Example

In the following example, we define an array named myArray with three elements of type integer, and assign 10 to the first element, 20 to the second element, and 30 to the last element.

int[] myArray;
myArray = new int[3]; // 3 is the size declarator
// We can now store 3 ints in this array,
// at index 0, 1 and 2

myArray[0] = 10; // 0 is the subscript, or index
myArray[1] = 20;
myArray[2] = 30;

If we were to try to store a fourth value in our array, at index 3, using e.g.

myArray[3] = 40;

our program would compile just fine, which may seems surprising. However, when executing this program, array bounds checking would be performed and detect that there is a mismatch between the size of the array and the index we are trying to use, resulting in a quite explicit error message:

Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds of the array at Program.Main()

Abridged Syntaxes

If you know the number of elements when you are defining an array, you can combine declaration and assignment on one line as follows:

<type>[] arrayName = new <type>[<number of elements>];

So, we can combine the first two lines of the previous example and write:

int[] myArray = new int[3];

We can even initialize and give values on one line:

int[] myArray = new int[3] { 10, 20, 30 };

And that statement can be rewritten as any of the following:

int[] myArray = new int[] { 10, 20, 30 };
int[] myArray = new[] { 10, 20, 30 };
int[] myArray = { 10, 20, 30 };

But, we should be careful, the following would cause an error:

int[] myArray = new int[5];
myArray = { 1, 2 ,3, 4, 5}; // ERROR

If we use the shorter notation, we have to give the values at initialization, we cannot re-use this notation once the array has been created.

Other datatypes, and even objects, can be stored in arrays in a perfectly similar way:

string[] myArray = { "Bob", "Mom", "Train", "Console" };

// Assume there is a class called Rectangle.
Rectangle[] arrayOfRectangle = new Rectangle[5];  

Simple Loops and Length

Custom Size and Loops

One of the benefits of arrays is that they allow you to specify the number of their elements at run-time: the size declarator can be a variable, not just an integer literal. Hence, depending on run-time conditions such as user input, we can have enough space to store and process any number of values.

In order to access the elements of whose size is not known until run-time, we will need to use a loop. If the size of myArray comes from user input, it wouldn’t be safe to try to access a specific element like myArray[5], because we cannot guarantee that the array will have at least 6 elements. Instead, we can write a loop that uses a counter variable to access the array, and use the loop condition to ensure that the variable does not exceed the size of the array.

Example

In the following example, we get the number of elements at run-time from the user, create an array with the appropriate size, and fill the array.

Console.WriteLine("What is the size of the array that you want?");
int size = int.Parse(Console.ReadLine());
int[] customArray = new int[size];

int counter = 0;
while (counter < size)
{
    Console.WriteLine($"Enter the {counter + 1}th value");
    customArray[counter] = int.Parse(Console.ReadLine());
    counter++;
}

Observe that:

The Length Property

Every single-dimensional array has a property called Length that returns the number of the elements in the array (or size of the array).

To process an array whose size is not fixed at compile-time, we can use this property to find out the number of elements in the array.

Example

int counter2 = 0;
while (counter2 < customArray.Length)
{
    Console.WriteLine($"{counter2}: {customArray[counter2]}.");
    counter2++;
}

Observe that this code does not need the variable size.

Note: You cannot use the length property to change the size of the array, that is, entering

int[] test = new int[10];
test.Length = 9;

would return, at compile time,

Compilation error (line 8, col 3): Property or indexer 'System.Array.Length' cannot be assigned to --it is read only.

When a field is marked as ‘read only,’ it means the attribute can only be initialized during the declaration or in the constructor of a class. We receive this error because the array attribute, ‘Length,’ can not be changed once the array is already declared. Resizing arrays will be discussed in the section: Changing the Size.

For Loops With Arrays

int[] homeworkGrades = {89, 72, 88, 80, 91};
int counter = 0;
int sum = 0;
while(counter < 5)
{
    sum += homeworkGrades[counter];
    counter++
}
double average = sum / 5.0;
int sum = 0;
for(int i = 0; i < 5; i++)
{
    sum += homeworkGrades[i];
}
double average = sum / 5.0;
Console.WriteLine("How many grades are there?");
int numGrades = int.Parse(Console.ReadLine());
int[] homeworkGrades = new int[numGrades];
for(int i = 0; i < numGrades; i++)
{
    Console.WriteLine($"Enter grade for homework {i+1}");
    homeworkGrades[i] = int.Parse(Console.ReadLine());
}
int sum = 0;
for(int i = 0; i < homeworkGrades.Length; i++)
{
    sum += homeworkGrades[i];
}
double average = (double) sum / homeworkGrades.Length;

Default Values and Resizing

When created, arrays have a fixed size and are populated with some default values. We discuss here what those default values are, how an array can be resized, and how we can avoid resizing an array.

Default Values

If we initialize an array but do not assign any values to its elements, each element will get the default value for that element’s data type. (These are the same default values that are assigned to instance variables if we do not write a constructor, as we learned in “More Advanced Object Concepts”). In the following example, each element of myArray gets initialized to 0, the default value for int:

int[] myArray = new int[5];
Console.WriteLine(myArray[2]); // Displays "0"
myArray[1]++;
Console.WriteLine(myArray[1]); // Displays "1"

However, remember that the default value for any object data type is null, which is an object that does not exist. Attempting to call a method on a null object will cause a run-time error of the type System.NullReferenceException;

Rectangle[] shapes = new Rectangle[3];
shapes[0].SetLength(5);  // ERROR

Before we can use an array element that should contain an object, we must instantiate an object and assign it to the array element. For our array of Rectangle objects, we could either write code like this:

Rectangle[] shapes = new Rectangle[3];
shapes[0] = new Rectangle();
shapes[1] = new Rectangle();
shapes[2] = new Rectangle();

or use the abridged initialization syntax as follows:

Rectangle[] shapes = {new Rectangle(), new Rectangle(), new Rectangle()};

Changing the Size

There is a class named Array that can be used to resize an array. Upon expanding an array, the additional indices will be filled with the default value of the corresponding type. Shrinking an array will cause the data in the removed indices (those beyond the new length) to be lost.

Example

Array.Resize(ref myArray, 4); //myArray[3] now contains 0
myArray[3] = 40;
Array.Resize(ref myArray, 2);

In the above example, all data starting at index 2 is lost.

Partially Filled Arrays

To avoid resizing an array, it also possible to declare it larger than it needs to be, and then to manipulate an accompanying integer variable that holds the number of elements that are actually stored in the array. The solution to the todo list project illustrates this behavior in detail, the general idea is that you want to let the user store some elements without having to say ahead of time how many, and without having to resize the array constantly. The drawback is that the Length property becomes less useful, and that you have to manipulate a custom “accounting” variable to keep track of the actual number of elements manipulated.

using System;

public class Program
{
  public static void Main(string[] args)
  {
    // We decide that the maximum number of input is 10.
    const int MAXSIZE = 10;

    int[] inputs = new int[MAXSIZE];

    // The following variable will contain the number of input actually given.
    int numberOfInputs = 0;

    // The following variable will hold the user input.
    string uInput;

    do
    {
      Console.WriteLine(
        "What is your input #"
          + (numberOfInputs + 1)
          + "? Enter \"done\" when you are done."
      );
      uInput = Console.ReadLine();
      if (uInput != "done")
      {
        inputs[numberOfInputs] = int.Parse(uInput);
        numberOfInputs++; // We increment the number of items in the list.
      }
      if (numberOfInputs == MAXSIZE)
      {
        Console.WriteLine(
          "You have reached the maximum number of inputs."
        );
      }
    } while (uInput != "done" && numberOfInputs < MAXSIZE);
    /*
     * When the user enters "done", or if the user reached the maximum number of inputs, we exit this loop.
     */
  }
}

Searching in Arrays

We now discuss how we can search for values in an array.

Finding the Maximum Value

To find the greatest value in an array of integer, one needs a comparison point, a variable holding “the greatest value so far”. Once this value is set, then one “just” have to inspect each value in the array, and to update “the greatest value so far” if the value currently inspected is greater, and then to move on to the next value. Once we reach the end of the array, we know that “the greatest value so far” is actually the greatest value (period) in the array.

The problem is to find the starting point: one cannot assume that “the greatest value so far” is 0 (what if the array contains only negative values?), so the best strategy is simply to assume that “the greatest value so far” is the first one in the array (after all, it is the greatest value we have seen so far).

Using foreach, we have for example the following:

int[] arrayExample = { 1, 8, -12, 9, 10, 1, 30, 1, 32, 3 };

int maxSoFar = arrayExample[0];
foreach (int i in arrayExample)
    if (i > maxSoFar) maxSoFar = i;

Console.WriteLine("The greatest value is "
    + maxSoFar + ".");

Finding a Particular Value

Suppose we want to set a particular Boolean variable to true if a particular value target is present in an arrayy arrayExample. The simplest way to perform such a search is to

  1. Set the Boolean variable to false,
  2. Inspect the values in arrayExample one by one, comparing them to target, and setting the Boolean variable to true if they are identical.
int[] arrayExample = { 1, 8, -12, 9, 10, 1, 30, 1, 32, 3 };

bool foundTarget = false;
int target = 8;

for (int i = 0; i < arrayExample.Length; i++)
{
    if (arrayExample[i] == target) foundTarget = true;
}
Console.WriteLine(target + " is in the array: " + foundTarget + ".");

Note that in the particular example above, we could have stopped exploring the array after the second index, since the target value was found. A slightly different logic would allow to exit prematurely the loop when the target value is found:

int[] arrayExample = { 1, 8, -12, 9, 10, 1, 30, 1, 32, 3 };

bool foundYet = false;
int target = 30;
int index = 0;

do
{
    if (arrayExample[index] == target) foundYet = true;
    index++;
}
while (index < arrayExample.Length && !foundYet);
Console.WriteLine(target + " is in the array: " + foundYet + 
"\nNumber of elements inspected: " + (index) +".");

This code would display:

30 is in the array: True
Number of elements inspected: 7.

Both codes are examples of linear (or sequential) search: the array is parsed one element after the other, and potentially all elements are inspected.

Finding a Particular Value in a Sorted Array

If the array is sorted (that is, the value at index i is less than the value at index i + 1), then the search for a particular value can be sped up by using binary search.

Sorted Arrays

A way of making sure that an array is sorted is given below. Note that, as above when trying to find the maximum value, we decide that the array is “sorted so far” unless proven otherwise, in which case we exit prematurely the loop. Note also that the condition contains index + 1 < arrayExample.Length: we need to make sure that “the next value” actually exists before comparing it with the current value.

int[] arrayExample = { 1, 10, 12, -1};
bool sortedSoFar = true;
int index = 0;

while (index + 1 < arrayExample.Length && sortedSoFar)
{
    if (arrayExample[index] > arrayExample[index+1]) sortedSoFar = false;
    index++;
}
Console.WriteLine("The array is sorted: " + sortedSoFar +".");

Introduction

Binary (half-interval or logarithmic) search leverages the fact that the array is sorted to speed up the search for a particular value. It goes as follows:

The algorithm compares the target value to the middle element of the array.

  1. If they are equal, we are done.

  2. If they are not equal, then there are two cases:

    1. If the middle element is greater than the target, then the algorithm restarts, but looking for the value only in the left half of the array,
    2. If the middle element is less than the target, then the algorithm restarts, but looking for the value only in the right half of the array.
  3. If the search ends with the remaining half being empty, the target is not in the array.

First Example

An example of implementation (and of execution) is as follows:

int[] arrayExample = { 1, 10, 12, 129, 190, 220, 230, 310, 320, 340, 400, 460};
bool foundSoFar = false;

int target = 340;

int start = 0;
int end = arrayExample.Length - 1;
int mid;
while (start <= end && !foundSoFar)
{
    mid = (start + end) / 2;
    /*
     * This is integer division: if start + end is odd,
     * then it will be truncated. In our example, 
     * (0 + 11) / 2 gives 5.
     */
    Console.WriteLine("The middle index is " + mid + ".");
    if (target == arrayExample[mid])
    {
        foundSoFar = true;
    }
    else if (target > arrayExample[mid])
    {
        start = mid + 1;
        Console.WriteLine("I keep looking right.");
    }
    else
    {
        end = mid - 1;
        Console.WriteLine("I keep looking left.");
    }
}
Console.WriteLine("Found the value: " + foundSoFar +".");

This code would display:

The middle index is 5.
I keep looking right.
The middle index is 8.
I keep looking right.
The middle index is 10.
I keep looking left.
The middle index is 9.
Found the value: True.

Second Example

Remembering that characters are such that 'A' is less than 'a', and 'a' is less than 'b', we can run a binary search on a sorted array of characters. The code below is the same algorithm as above, only the information logged changes:

char[] arrayExample = { 'A', 'B', 'D', 'Z', 'a', 'b', 'd' };
char target = 'D';
bool foundSoFar = false;
int start = 0;
int end = arrayExample.Length - 1;
int mid;
while (start <= end && !foundSoFar)
{
    Console.WriteLine("Range: " + start + " -- " + end);
    mid = (start + end) / 2;
    Console.WriteLine("Mid: " + mid);
    if (target == arrayExample[mid])
    {
        foundSoFar = true;
    }
    else if (target > arrayExample[mid])
    {
        start = mid + 1;
    }
    else
    {
        end = mid - 1;
    }
}
Console.WriteLine("Found the value: " + foundSoFar + ".");

This will display:

Range: 0 -- 6
Mid: 3
Range: 0 -- 2
Mid: 1
Range: 2 -- 2
Mid: 2
Found the value: True

Observe that if we were to replace start <= end with start < end then the algorithm would not have correctly terminated in the example above.

Arrays of Objects

An array can contain more than simple datatypes: it can contains object. It can be objects from a custom class, or even … arrays, which are themselves objects!

Array of Objects From a Custom Class

In the following example, we will ask the user how many Item objects (the details of the implementation does not matter, but can be inspired by this example) they want to create, then fill an array with Item objects initialized from user input:

Console.WriteLine("How many items would you like to stock?");
Item[] items = new Item[int.Parse(Console.ReadLine())];
int i = 0;
while(i < items.Length)
{
    Console.WriteLine($"Enter description of item {i+1}:");
    string description = Console.ReadLine();
    Console.WriteLine($"Enter price of item {i+1}:");
    decimal price = decimal.Parse(Console.ReadLine());
    items[i] = new Item(description, price);
    i++;
}

Observe that, since we do not perform any user-input validation, we can simply use the result of int.Parse() as the size declarator for the items array - no size variable is needed at all.

We can also use while loops to search through arrays for a particular value. For example, this code will find and display the lowest-priced item in the array items, which was initialized by user input:

Item lowestItem = items[0];
int i = 1;
while(i < items.Length)
{
    if(items[i].GetPrice() < lowestItem.GetPrice())
    {
        lowestItem = items[i];
    }
    i++;
}
Console.WriteLine($"The lowest-priced item is {lowestItem}");

Note that the lowestItem variable needs to be initialized to refer to an Item object before we can call the GetPrice() method on it; we cannot call GetPrice() if lowestItem is null. We could try to create an Item object with the “highest possible” price, but a simpler approach is to initialize lowestItem with items[0]. As long as the array has at least one element, 0 is a valid index, and the first item in the array can be our first “guess” at the lowest-priced item.

Arrays of Arrays

An array of arrays is called a multi-dimensional array. A multi-dimensional array can be rectangular (it then represents an n-dimensional block of memory) or jagged (in that case, it is an array of arrays).

Rectangular Multi-Dimensional Array

Also called 2-dimensional arrays, their syntax is very close to 1-dimensional arrays:

int[,] matrix = new int[2, 3];

where 2 is the number of rows, and 3 is the number of columns. They can be accessed with matrix.GetLength(0) and matrix.GetLength(1) respectively.

Assignment is as for 1-dimensional arrays, starting at 0:

matrix[0, 0] = 1;
matrix[0, 1] = 2;
matrix[0, 2] = 3;
matrix[1, 0] = 4;
matrix[1, 1] = 5;
matrix[1, 2] = 6;

This will produce a matrix as follows:

  0th col. 1st col. 2nd col.
0th row 1 2 3
1st row 4 5 6

We could also have used a shortened notation to declare this 2-dimensional array, as follows:

int[,] matrix = new int[,]
{
    {1,2,3},
    {4,5,6}
};

or even simply

int[,] matrix = {{1,2,3},{4,5,6}};

To display such an array, nested loops are needed:

for (int row = 0; row < matrix.GetLength(0); row++)
{
    for (int col = 0; col < matrix.GetLength(1); col++)
        Console.Write(matrix[row, col] + " ");
    Console.WriteLine();
}

Jagged Array

A jagged array is an array of arrays. The difference with rectangular arrays is that the arrays stored can be of varying size.

The syntax is straightforward once understood that jagged arrays are exactly arrays of arrays:

int[][] jaggedArray = new int[3][];
jaggedArray[0] = new int[3] { 1, 2, 3 };
jaggedArray[1] = new int[2] { 4, 5 };
jaggedArray[2] = new int[5] { 6, 7, 8, 9, 10 };

for (int row = 0; row < jaggedArray.Length; row++)
{
  Console.Write("The row #" + row + " contain: ");
  for (
    int arrayCell = 0;
    arrayCell < jaggedArray[row].Length;
    arrayCell++
  )
  {
    Console.Write(jaggedArray[row][arrayCell] + " ");
  }
  Console.WriteLine("");
}

In this example, it should be clear that jaggedArray[row] is itself an array, and hence that we can use e.g., jaggedArray[row].Length or jaggedArray[row][arrayCell].

Manipulating Rectangular Arrays

We present below some simple algorithms to manipulate 2-dimensional (rectangular) arrays. The code for this lecture is available in this archive.

Summing the values row per row

The following code sum the values contained in a 2-dimensional array row per row, and display the result each time before moving on to the next row:


    int[,] numbers =
    {
      { 1, 2, 3, 4 },
      { 5, 6, 7, 8 },
    };

    int acc;
    for (int row = 0; row < numbers.GetLength(0); row++)
    {
      acc = 0;
      for (int col = 0; col < numbers.GetLength(1); col++)
      {
        acc += numbers[row, col];
      }
      Console.WriteLine(
        "Total for row #" + row + " is " + acc + "."
      );
    }

    //

This code can easily be adapted to compute the sums column per column if needed.

Computing Magic Square

A magic square is a square matrix where the sums of the numbers in each row, each column, and both the diagonal and the anti-diagonal are the same.

The following is an example of a magic square:

    int[,] arrayP1 =
    {
      { 4, 9, 2 },
      { 3, 5, 7 },
      { 8, 1, 6 },
    };

as we have, diagonally, 4 + 5 + 6 = 15

and anti-diagonally, 2 + 5 + 8 = 15

and on the rows, 4 + 9 + 2 = 15 3 + 5 + 7 = 15 8 + 1 + 6 = 15

and finally on the columns 4 + 3 + 8 = 15 9 + 5 + 1 = 15 2 + 7 + 6 = 15

A method to return true if the 2d-matrix of int passed as an argument is a magic square is as follows:

static class MagicSquare
{
  public static bool isMagic(int[,] arrayP)
  {
    bool magicSoFar = true;
    if (arrayP.GetLength(0) == arrayP.GetLength(1))
    { // The array is a square.
      int magicConstant = 0;
      for (int i = 0; i < arrayP.GetLength(1); i++)
      {
        magicConstant += arrayP[i, i];
      }
      int testedValue = 0;
      for (int i = 0; i < arrayP.GetLength(1); i++)
      {
        testedValue += arrayP[
          i,
          arrayP.GetLength(1) - i - 1
        ];
      }
      if (testedValue == magicConstant)
      { // The diagonal and anti-diagonal have the same sums.
        // We test the rows.
        for (int row = 0; row < arrayP.GetLength(0); row++)
        {
          testedValue = 0;
          for (
            int col = 0;
            col < arrayP.GetLength(1);
            col++
          )
          {
            testedValue += arrayP[row, col];
          }

          if (testedValue != magicConstant)
          {
            magicSoFar = false;
          }
        }
        // We test the columns.
        for (int col = 0; col < arrayP.GetLength(1); col++)
        {
          testedValue = 0;
          for (
            int row = 0;
            row < arrayP.GetLength(0);
            row++
          )
          {
            testedValue += arrayP[row, col];
          }

          if (testedValue != magicConstant)
          {
            magicSoFar = false;
          }
        }
      }
      else
      { // The diagonal and anti-diagonal have different same sums.
        magicSoFar = false;
      }
    }
    else
    { // The array is not a square.
      magicSoFar = false;
    }

    return magicSoFar;
  }
}

Over and Underflow

Overflow

using System;

class Program
{
  static void Main()
  {
    uint n1,
      n2;

    Console.WriteLine(
      "Enter the requested loan amount for the first person:"
    );
    n1 = uint.Parse(Console.ReadLine());

    Console.WriteLine(
      "Enter the requested loan amount for the second person:"
    );
    n2 = uint.Parse(Console.ReadLine());

    if (n1 + n2 < 10000)
    {
      Console.WriteLine($"Pay ${n1} to the first person");
      Console.WriteLine($"Pay ${n2} to the second person");
    }
    else
    {
      Console.WriteLine(
        "Error: the sum of the loans exceeds the maximum allowance."
      );
    }
  }
}

Underflow

using System;

class Program
{
  static void Main()
  {
    float myNumber;
    myNumber = 1E-45f;
    Console.WriteLine(myNumber); //outputs 1.401298E-45
    myNumber = myNumber / 10;
    Console.WriteLine(myNumber); //outputs 0
    myNumber = myNumber * 10;
    Console.WriteLine(myNumber); //outputs 0
    myNumber = (1E-45f / 10) * 10;
    Console.WriteLine(myNumber); //outputs 0
  }
}

Random

Exceptions

Introduction

Syntax and Rules for trycatchfinally Statements

Exception Class and Objects

Purpose of the finally Block

Scoping in trycatchfinally Statements

When To Use trycatch and When To Use TryParse?

Throwing an Exception

The List collections

Introduction

The List class serves a similar purpose than arrays, but with a few notable differences:

Syntax

Creation

The syntax to create an empty list of string named nameList and a list of int named valueList containing 1, 2 and 3 is:

List<string> nameList = new List<string>();
List<int> valueList = new List<int>() { 1, 2, 3 };

Adding Elements

Adding an element to the list is done using the Add method, and counting the number of elements is done using the Count property:

Console.WriteLine("nameList has " + nameList.Count + " element.");
nameList.Add("Bob");
Console.WriteLine("nameList has " + nameList.Count + " element.");
nameList.Add("Sandrine");
Console.WriteLine("nameList has " + nameList.Count + " elements.");

Note that we did not need to resize the nameList manually: its size went from 0 to 1 after we added “Bob”, and from 1 to 2 after we added “Sandrine”.

Accessing Elements

Using the [] operator

Accessing an element can be done using the same operator as with arrays (the [] operator):

Console.Write(nameList[0]);

will display “Bob”. Note that this syntax can be used to change the value of an element that already exist. For example,

nameList[0] = "Robert";

would replace the first value in the list (“Bob”) with “Robert”.

Note that while accessing or replacing an element using the [] operator inside a list is fine, you cannot add new elements to the list using this syntax. For example,

nameList[2] = "Sandrine";

would raise an exception since there is no third element to our list.

Using foreach

Another way of accessing the elements in a list is to use foreach loops:

foreach (string name in nameList)
{
    Console.WriteLine(name); 
}

Removing Elements

An element can be removed from the list using the RemoveAt method. If nameList contains “Robert, Sandrine”, then after the following statement,

nameList.RemoveAt(0);

it would only contain “Sandrine” and its size would be 1. That is, the first element would be deleted and the list would shrink.

Another way of removing an element is to use the Remove method. Suppose we have the following list:

List<int> valueList = new List<int>() {-1, 0, 1, 2, 3, 2, 5 };

then using

valueList.Remove(1);

would remove “1” from the list, and the list would become -1, 0, 2, 3, 2, 5.

Observe that Remove returns a bool, so that for instance the following

if(valueList.Remove(0)){
    Console.WriteLine("0 was removed.");
}

would not only remove 0 from the list, but also display “0 was removed”.

Finally, if the value is present multiple times in the list, then only its first occurrence is removed. For example, if the list is -1, 2, 3, 2, 5, then after executing

valueList.Remove(2);

it would become -1, 3, 2, 5.

A Custom Implementation of Lists

A “custom” implementation of list can be found in this project.

Reference Types

Motivation

There is a fundamental difference between value types and reference types in C#. For example, compare:

int x = 10;
int y = x;
y = 11;
Console.WriteLine($"x is {x}, y is {y}.");
// Displays "x is 10, y is 11.".

and

int[] a = { 10 };
int[] b = a;
b[0] = 11;
Console.WriteLine($"a[0] is {a[0]}, b[0] is {b[0]}.");
// Displays "a[0] is 11, b[0] is 11.".

In the first case (with ints), the value of x will remain 11, but in the second (with arrays of ints), a[0] will now contain 11 as well. That is because when y = x was executed, the value of x was copied, but when b = a is executed, the reference to the array was copied.

All the built-in types are value types: numerical types, char and bool contains values. On the other hand, objects, string and arrays, for example, are reference types.

null Value

Reference types can contain a special value, called null, that intuitively means that it references nothing. It can be used as follows:

int[] c = null;

Any reference type must be handled with great care, since for example

Console.WriteLine(c.Length);

would compile but would throw a NullReferenceException exception (a null reference doesn’t have any Length property!).

Three operators allows to simplify testing whenever a variable holds null and behave accordingly, we detail them below.

null-Conditional Operator

The null-conditional operator ? allows to test if a variable holds null and to avoid some NullReferenceException.

For example,

Console.WriteLine($"Length of a is: {a?.Length}.");

will display “Length of a is: 1.” if a holds a reference to an array of size 1, and “Length of a is: .” if a holds a null. Stated differently, a?.Length evaluates to the size of the array referenced by a if it exists, to null otherwise.

One can similarly write a?[0] to either get a null (if a itself is null) or the value at the first index of the array referenced by a.

null-Coalescing Operator

The null-coalescing operator ?? allows to assign a reference if it is not null, and to assign a default value otherwise.

For example,

string s1 = null;
string s2 = s1 ?? "nothing";
Console.WriteLine($"s1 is {s1}, s2 is {s2}.");

will display “s1 is , s2 is nothing.”: the assignment s2 = s1 ?? "nothing" “skipped” the value s1 since it was null and used "nothing" instead.

null-Coalescing Assignment Operator

The null-coalescing assignment operator ??= allows to re-assign a variable if it is null.

For example,

s1 ??= "default";

will assign "default" to s1 if it is null, leave its value unchanged otherwise. Note that this operator is available only starting with C# 8.0.

Nullable value types

It is also possible to make a value type nullable, so that it can contains the null value. For example,

int[] a = null;
int aLength = a?.Length;

is not valid since a?.Length will evaluate to null, and an int variable cannot contain a reference!

It is possible, however, to make aLength nullable, using the ? operator:

int[] a = null;
int? aLength = a?.Length;

This way, aLength can contain either an integer value, or the null reference.

To “convert” a nullable value type back into a “non-nullable” value type can be done using the null-coalescing operator ??. For example,

int d = aLength ?? -1;

will assign aLength to d if it is not null, and -1 otherwise: note that either way, d will end up containing a non-null value.

Testing for Equality

Motivation

A great care is required when comparing references, since one need to make sure that

A “shallow” comparison compares only the “surface” of reference variables, as follows:

int[] a = { 10 };
int[] b = a;
int[] c = { 10 };

if (a == b){ Console.WriteLine("a and b refers the same array."); }
if (a != c){ Console.WriteLine("a and c refers different arrays."); }

Both tests would evaluate to true, since a and b do indeed refer to the same array, while a and c refer to different arrays. In general, this is not what is intended when comparing objects or arrays: we want to know if what they refer to is identical.

Comparing Arrays

To compare arrays while accounting for possible null values, a great care is needed. One can write a method as follows:

public static bool SameArray<T>(T[] arP1, T[] arP2)
{
    if (arP1 == null && arP2 == null) { return true; }
    else if (arP1 == null || arP2 == null) { return false; }
    else if (arP1.Length != arP2.Length) return false;
    else {
        for (int i = 0; i < arP1.Length; i++)
        {
            if (!Equals(arP1[i], arP2[i])) return false;
        }
    }
    return true;
}

So that, if SameArray is passed…

Note that

Passing Arguments

Motivation

Consider the following “swapping” method and a Main method calling it:

using System;

class Program
{
  static void Main()
  {
    int a = 10;
    int b = 20;
    Console.WriteLine(
      $"Before swap: a holds {a}, b holds {b}."
    );
    Swap(a, b);
    Console.WriteLine(
      $"After swap:  a holds {a}, b holds {b}."
    );
  }

  static void Swap(int a, int b)
  {
    int temp = a;
    a = b;
    b = temp;
    Console.WriteLine(
      $"Inside swap: a holds {a}, b holds {b}."
    );
  }
}

This program would display:

Before swap: a holds 10, b holds 20.
Inside swap: a holds 20, b holds 10.
After swap:  a holds 10, b holds 20.

As we can see, the values held by the variables a and b are correctly swapped by the Swap method, but this change is not “permanent”: once the Swap method completed, a and b still have their “old” values inside Main.

Since a method cannot return two values, making that change permanent is difficult. A solution could be designed using arrays for example, but it would require additional manipulation in the Main method. Instead, one can use references to pass the reference to the variables instead of their values.

ref Keyword

The ref keyword can be used to pass the reference to a variable, as follows:

using System;

class Program
{
  static void Main()
  {
    int a = 10;
    int b = 20;
    Console.WriteLine(
      $"Before swap: a holds {a}, b holds {b}."
    );
    Swap(ref a, ref b);
    Console.WriteLine(
      $"After swap:  a holds {a}, b holds {b}."
    );
  }

  static void Swap(ref int a, ref int b)
  {
    int temp = a;
    a = b;
    b = temp;
    Console.WriteLine(
      $"Inside swap: a holds {a}, b holds {b}."
    );
  }
}

Note that the change with the previous code is minimal: only the keyword ref is added:

Note that both edits are required: the first one stipulates that the Swap method expects references, and the second one stipulates that the references are passed.

This program would display:

Before swap: a holds 10, b holds 20.
Inside swap: a holds 20, b holds 10.
After swap:  a holds 20, b holds 10.

Indeed, since the reference was passed, Swap stored the new values in the same variables a and b, making the swapping “permanent”.

out Keyword

In some cases, one may want to pass a reference to a method simply as an address where a value must be stored. The benefit is that this reference does not need to contain a value before being passed to a method.

For example, consider:

static void SetToRandom(ref int a)
{
    Random gen = new Random();
    a = gen.Next(10);
}

that sets the value of a reference to a random number between 0 and 9 (both included).

It cannot be called as follows:

int a; // This code will not compile
SetToRandom(ref a);

Because C#’s compilation will return the error message “Use of unassigned local variable ‘c’”. Indeed, SetToRandom expects the argument to already holds a reference to a value, even if it has no use for it.

A better alternative is to use the out keyword:

using System;

class Program
{
  static void Main()
  {
    int a;
    SetToRandom(out a);
    Console.WriteLine(a);
  }

  static void SetToRandom(out int a)
  {
    Random gen = new Random();
    a = gen.Next(10);
  }
}

Note that:

Summing up, the difference between ref and out is that out does not require the reference to point to an actual value entering into the method but it must hold a value by the time we exit the method.

To illustrate this last point, observe that

static void Dummy(out int a)
{
    Console.WriteLine("Hi!");
}

would not compile, as C# would give back a message “The out parameter ‘a’ must be assigned to before controls leaves the current method”: an argument passed using the keyword out must be initialized in the body of the method.

Files

The code for this lecture is available in this archive.

Motivation

Files are useful for permanency: when a program terminates, all the objects created, strings entered by the user, and even the messages displayed on the screen, are lost. Saving some information as files allows to retrieve this information after the program terminates, possibly using a different tool. Retrieving information from a file allows to continue works that was started by a different program, or to use a better-suited tool to carry on some task (typically, counting the number of words in a document).

This lecture is concerned with files: how to write into them, how to read from them, how to make sure that our program does not throw exceptions when dealing with “I/O” (read: input / output) operations? We will only consider textual files, with the .txt extension, for now.

Warm-Up: Finding a Correct Path

Each file has a (full, or absolute) path, which is a string describing where it is stored: it is made of a directory path and a file (or base) name. Paths are complicated because they can

A fairly reliable way of handling this diversity is to select the folder /bin/Debug naturally present in your solution (it is where the executable is stored). We can access its directory path using:

    string directoryPath = AppDomain
      .CurrentDomain
      .BaseDirectory;
    Console.WriteLine(
      "Directory path is " + directoryPath + "."
    );

On most Unix systems, this would display at the screen something like

~/source/code/projects/FileDemo/FileDemo/bin/Debug/

To add to this directory path the file name, we will be using the Combine method from the Path class, as follows:

    string filePath = Path.Combine(
      directoryPath,
      "Hello.txt"
    );
    Console.WriteLine("File path is " + filePath + ".");
⚠ Warning
Unless otherwise stated, we will always use this folder in our examples.

Writing Into a File

Writing into a file is done using the StreamWriter class and a couple of methods:

Even if we will not go into details about the role of the Close method, it is extremely important and should never be omitted.

As an example, we can create a HelloWorld.txt file containing

Hello World!!
From the StreamWriter class0123456789

using the following code:

      StreamWriter sw = new StreamWriter(filePath);
      sw.WriteLine("Hello World!!");
      sw.Write("From the StreamWriter class");
      for (int x = 0; x < 10; x++)
      {
        sw.Write(x);
      }
      sw.Close();

Reading From a File

Reading from a file is very similar to writing from a file, as we are using a StreamReader class and a couple of methods:

The Close method is similarly important and should never be omitted.

As an example, we can open a file located at filePath and display its content at the screen using:

        string line;
        StreamReader sr = new StreamReader(filePath);
        line = sr.ReadLine();
        while (line != null)
        {
          Console.WriteLine(line);
          line = sr.ReadLine();
        }
        sr.Close();

What Can Go Wrong?

When manipulating files, many things can go wrong.

What if we are trying to read a file that does not exist?

If the StreamReader constructor is given a path to a file that does not exist, it will raise an exception. We can catch this exception, but a better mechanism is to simply warn the user, using the File.Exists method that return true if its string argument points to a file, false otherwise.

      if (!File.Exists(filePath))
      {
        Console.WriteLine("File does not exist.");
      }

What if we are trying to write into a file that already exist?

A dual problem is if the path we are using as an argument to the StreamWriter constructor points to a file that already exists: by default, that file will be overwritten. An alternative mechanism is to use the overloaded StreamWriter constructor that additionally takes a bool as argument: if the bool is set to true, then the new content will be appended into the existing file instead of overwriting it.

Hence, we can use the following:

StreamWriter sw = new StreamWriter(filePath, true);

with benefits:

The previous code

StreamWriter sw = new StreamWriter(filePath);

should be used only if we do not care about existing files.

Many Things Can Go Wrong!

We will not list in detail all the ways things can go wrong with manipulating files (memory shortage, access right limitations, concurrent access to a file, etc.), but read and write access to files should always take place in try{}catch{} blocks.


  1. Although that may sound curious, we believe it is important to remind you of the fact that they can only help you understanding, but that you have to do your part!↩︎

  2. That’s a job really well taken care of by the Academic Success Center!↩︎

  3. Unless this class is fully online, of course.↩︎

  4. That will be studied in the course of your study if you continue as a CS major.↩︎

  5. We use the notation [] to denote what “should” be there, but this is just a place holder: you are not supposed to actually have the braces in the code.↩︎

  6. Well, if there are no in-line comments in it. Can you figure out why?↩︎

  7. At this point, you may wonder “why don’t we always use the most precise datatype instead of using imprecise ones?”. There are three dimensions to consider to answer this question: first, using decimal takes more memory, hence more time, than the other numerical datatypes. Second, they are a bit more cumbersome to manipulate, as we will see later on. Last, you generally don’t need to be that precise: for example, it would not make sense to use a floating-point number to account for human beings or other indivisible units. Even decimal may be an overkill for floating-point values sometimes: for instance, the NASA uses 3.141592653589793 as an approximation of pi for their calculations. A double can hold such a value, so there is no need to be more precise.↩︎

  8. We use “bike” to refer to both bicycles and motorcycles.↩︎

  9. Technically, a “rectangular pyramid”, if we require the pyramid to have a rectangle as its base.↩︎

  10. Note that, this time, since our code below does not override the methods and properties, there really is no need to repeat them the derived classes.↩︎

  11. Except that the fork bomb calls itself twice, and in parallel.↩︎

  12. Usually, all the elements of an array have the same type, but an array can store elements of different types if object is its type, since any element is actually of type object.↩︎

  13. That is, unless the program crashes or loops forever.↩︎