Click here to Skip to main content
14,644,005 members
Articles » Languages » C# » Applications
Article
Posted 3 Apr 2019

Tagged as

Stats

811.3K views
4.2K downloads
544 bookmarked

List vs IEnumerable vs IQueryable vs ICollection vs IDictionary

Rate this:
4.89 (365 votes)
Please Sign up or sign in to vote.
4.89 (365 votes)
3 Apr 2019CPOL
This article is one of the first sources (with complete and clear comparison) about data structure, Array, ArrayList, List, IList, ICollection, Stack, Queue, HashTable, Dictionary, IQueryable, IEnumerable.

Complete Collection Comparison

Collection

Collection is a set of related records. It includes a meaningful unit:
We should select appropriate container to store data temporarily for fetch and modification process.
Appropriate container depends on:

  1. Our aim which we want to do on data (just reading, doing modification such as insert, delete, update)
  2. Number of records which should be transferred

Image 2

Array

  1. Fixed Length -> Its size is not flexible. It is determined at instantiation time.
  2. Strongly Typed -> Developers determine its type at the instantiation time instead of runtime. This feature makes to run fast at runtime, since it does not need to wait for type definition.
  3. Developers use "foreach" keyword to fill and iterate through array.

Fixed Length and Strongly Typed consume less memory, therefore it has good performance.

Image 3

//It is obvious that strArray is
//1. string   --> Strongly Type
//2. Sized=10 --> Fixed Size

string[] strArray = new string[10];

for (int i = 0; i < 10; i++)
{
    if (strArray[i]==null)
    {
        strArray[i] = (i+1).ToString();
    }
}

this.ListBoxArray.DataSource = null;
this.ListBoxArray.Items.Clear();

this.ListBoxArray.DataSource = strArray;
this.ListBoxArray.DataBind();

ArrayList

  1. Arraylist is NOT Fixed Length -> It is possible that data grows. On the one hand, it is a good feature whenever developers are not sure about size of arraylist and on the other hand, it might take a long time for size definition.
  2. Arraylist is NOT Strongly Typed -> Whenever developers are not sure about what exactly is type definition for input or output data, they should wait until runtime to appear its type. Its disadvantage is time consuming at runtime for memory to determine type definition.
  3. Developers use "foreach" keyword to fill and iterate through arraylist.

Image 4

public class Product
    {
        public Product()
        {
        
        }
        public Product(string Code, string Name)
        {
            _Code = Code;
            _Name = Name;
        }

		    public string _Code {get; set;}
            public string _Name { get; set; }
    }

ArrayList can accept string, integer and decimal simultaneously.

//It is NOT obvious that strArrayList is 1. string? int? object? decimal?  --> NOT Strongly Type
//                                       2. Sized=10? 20? 100?             -->NOT Fixed Size
// Namespace: System.Collections

System.Collections.ArrayList strArrayList = new System.Collections.ArrayList();
//System.Linq.IQueryable  type of data is not specific runtime deferred support
strArrayList.Add("Mahsa");  //   "Mahsa": is string
strArrayList.Add(1);        //        1 : is integer
strArrayList.Add(0.89);     //      0.89: is decimal

this.ListBoxArrayList.DataSource = null;
this.ListBoxArrayList.Items.Clear();
this.ListBoxArrayList.DataSource = strArrayList;
this.ListBoxArrayList.DataBind();

System.Text.StringBuilder str= new System.Text.StringBuilder();

foreach (var item in strArrayList)
{
    str.Append(" , "+item);
}
this.lblArrayList.Text = str.ToString();

//Below is old way to fill obj from product,
//in Arraylist you need to create more than one instance
// Product objProduct = new Product();
// objProduct.Code = "1001";
// objProduct.Name = "Chair";

//It is NOT obvious that strArrayList is
//1. string? int? object? decimal? OR OBJECT??  --> NOT Strongly Type
//2. Sized=10? 20? 100?                         -->NOT Fixed Size
// Namespace: System.Collections

System.Collections.ArrayList objArrayList = new System.Collections.ArrayList();

objArrayList.Add(new Product("1001", "Chair"));
objArrayList.Add(new Product("1002", "Sofa"));
objArrayList.Add(new Product("1003", "Carpet"));

this.DropDownListArrayListObject.DataSource = null;
this.DropDownListArrayListObject.Items.Clear();
this.DropDownListArrayListObject.DataSource = objArrayList;

//* Finding among Object of Array List is difficult,
//you have to find your specific item by index
Product objTemp = (Product)objArrayList[0];
objArrayList.Remove(objTemp);
//*
this.DropDownListArrayListObject.DataTextField = "_Name";
this.DropDownListArrayListObject.DataValueField = "_Code";
this.DropDownListArrayListObject.DataBind();
this.GridViewArrayListObject.DataSource = objArrayList;
this.GridViewArrayListObject.DataBind();

HashTable

HashTable is another kind of data structure that defines key value for each data section. Therefore finding data is easy just by pointing out to its key. It is NOT strongly typed and NOT fixed size.

Image 5

//It is NOT obvious that strArrayList is
//1. string? int? object? decimal? OR OBJECT??  --> NOT Strongly Type
//2. Sized=10? 20? 100?                         -->NOT Fixed Size
// Namespace: System.Collections
//Hashtable solve the problem in Arraylist when we are looking for specific item
//Hashtable dedicate a key for each item, then finding item is easier and faster

 System.Collections.Hashtable objHashTable = new System.Collections.Hashtable();

 objHashTable.Add("1001","Chair");
 objHashTable.Add("1002", "Sofa");
 objHashTable.Add("1003", "Carpet");


this.DropDownListHashTable.DataSource = null;
this.DropDownListHashTable.Items.Clear();
this.DropDownListHashTable.DataSource = objHashTable;
//* finding item is easier you just need to point to it by call its key
objHashTable.Remove("1002");
//*
this.DropDownListHashTable.DataTextField = "Value";
this.DropDownListHashTable.DataValueField = "Key";
this.DropDownListHashTable.DataBind();

Stack

We have different data structure and stack is one of them. Stack is subset of data structure. Stack is a prioritized data structure (such as List is indexed base). Stack defines priority for each item, it means stack behavior forces its items to put (push) inside stack prioritized form. So stack put later item on the top of items and this behavior is "defining priority for each item". Therefore whenever you want to insert item, you should add (PUSH) it at the top of the stack and whenever you want to remove (POP) item from stack, you should remove it from top of the stack. As you got it, item that comes last will be selected to POP for first one and its expression in computer science is equal to "Last in First out" == "LIFO".

It is NOT strongly typed and NOT fixed size.

Image 6

Stack - Push

//Stack is LIFO: Last in First Out
System.Collections.Stack objStackPush = new System.Collections.Stack();

//By Push method, you can insert item at the top of the stack
objStackPush.Push("Mahsa");
objStackPush.Push("Hassankashi");
this.lblPop.Text = "";
this.ListBoxStack.DataSource = objStackPush.ToArray();
this.ListBoxStack.DataBind();

Stack - Pop

System.Collections.Stack objStackPop = new System.Collections.Stack();

objStackPop.Push("Mahsa");
objStackPop.Push("Hassankashi");

//By Pop method, you can remove item from the top of the stack --> Last in First in
this.lblPop.Text = objStackPop.Pop().ToString();

this.ListBoxStack.DataSource = objStackPop.ToArray();
this.ListBoxStack.DataBind();

Queue

Queue is another kind of data structure that defines priority for each item in other form. Therefore, whenever you want to insert item, you should add (Enqueue) it at the head of the queue and whenever you want to remove (Dequeue) item from queue, you should remove it from bottom of the queue. As you got it, item that comes first will be selected to Dequeue for first one and its expression in computer science is equal to "First in First out" == "FIFO".

It is NOT strongly typed and NOT fixed size.

Image 7

Queue - Enqueue

//Queue is FIFO: First in First Out
System.Collections.Queue objQueue = new System.Collections.Queue();

//By Enqueue method you can insert item at the END of the Queue
objQueue.Enqueue("Mahsa");
objQueue.Enqueue("Hassankashi");
objQueue.Enqueue("Cosmic");
objQueue.Enqueue("Verse");

this.lblQueue.Text = "";
this.ListBoxQueue.DataSource = objQueue.ToArray();
this.ListBoxQueue.DataBind();

Queue - Dequeue

System.Collections.Queue objQueue = new System.Collections.Queue();

objQueue.Enqueue("Mahsa");
objQueue.Enqueue("Hassankashi");
objQueue.Enqueue("Cosmic");
objQueue.Enqueue("Verse");

//By Dequeue method you can remove item from the BEGINNING of the Queue -->
//First in First out FIFO
this.lblQueue.Text=objQueue.Dequeue().ToString();

this.ListBoxQueue.DataSource = objQueue.ToArray();
this.ListBoxQueue.DataBind();

List

Why do we need List?

  1. List is NOT Fixed Length -> It is possible that data grows. On the one hand, it is a good feature whenever developers are not sure about size of arraylist and on the other hand, it might take a long time for size definition.
  2. List is Strongly Typed when it is defined "Generic" -> Whenever developers are sure about what is exactly type definition for input or output data and they do not wait until runtime to appear its type. This feature makes to run fast at runtime, since it does not need to wait for type definition.
  3. Developers use "foreach" keyword to fill and iterate through array.

Since List is not Fixed Length makes developers feel flexible to use it, and because of it is Strongly Typed when it is defined "Generic" so our code runs fast at runtime because it does not need to wait for type definition.

Image 8

//Like Array is Strong Type
//Like ArrayList with No Dimension
System.Collections.Generic.List<string> strList = new List<string>();

strList.Add("Mahsa");
strList.Add("Hassankashi");
strList.Add("Cosmic");
strList.Add("Verse");

this.ListBoxListGeneric.DataSource = strList;
this.ListBoxListGeneric.DataBind();

System.Text.StringBuilder str = new System.Text.StringBuilder();

foreach (var item in strList)
{
    str.Append(" , " + item);
}
this.lblList.Text = str.ToString();

IList

Why do we need IList? IList is implemented by List, Ilist is an interface and implements methods. Whenever you estimate probability that your code would be changed in future, you have to use IList because interface reduces dependency and with the little modification, your code runs. Therefore, you should observe polymorphism in order to decouple your app and control on adding or removing method that might be changed. Everything else is similar. Whenever we want to change on some operation, so “IList” allows us to do that easily with at least changing in the whole of codes.

Interfaces cannot be instantiated, so it should be instantiated from List.

System.Collections.Generic.IList<string> strIList = new List<string>();

Difference between Concrete Class and Interface

  1. Concrete Class inherits from just ONE class but it can implements one or MORE than one interfaces.
  2. You can write inside concrete class full version of your function while you have to define just signature inside the interface.
  3. You can define variable and value Inside Concrete Class while you are not allowed to define variable inside interfaces.
  4. A Concrete class can have constructor while you are not allowed to define constructor inside interfaces.
  5. Abstract class can contain access modifier while interfaces does not.

As I mentioned that how a class cannot be driven from two classes, it just can be driven from only one class, so whenever you want to drive from two classes, it is not possible to inherit from two abstract classes, but it is possible to drive from more than one class by interfaces.

In future, if developers decide to add some features to their class and inherit it from another class, developers always prefer to use interface of collection so that if you want to change your code and enhance its abilities, choose interfaces.

On the other hand, interface keeps your program extensible and Decouple: The classes are independent from each other, so error, exception and failure will happen rarely in future changing code by interfaces.

Image 9

Polymorphism: When you are using interface, you absolutely observe and do polymorphism and OOP. It means encapsulate your designer. Interface means a point or a node to join two parts to each other, it means making low dependency from two parts and making a joint section to make flexible change in future.

//Ilist can not be instantiate from Ilist , so it should be instantiate from List
System.Collections.Generic.IList<string> strIList = new List<string>();

strIList.Add("Mahsa");
strIList.Add("Hassankashi");
strIList.Add("Cosmic");
strIList.Add("Verse");

this.ListBoxListGeneric.DataSource = strIList;
this.ListBoxListGeneric.DataBind();

System.Text.StringBuilder str = new System.Text.StringBuilder();

foreach (var item in strIList)
{
    str.Append(" , " + item);
}
this.lblList.Text = str.ToString();

IEnumerable

IEnumerable is suitable just for iterating through collection and you cannot modify (Add or Remove) data. IEnumerable brings ALL data from server to client, then filters them, assume that you have a lot of records so IEnumerable puts overhead on your memory.

Image 10

//IEnumerable can not be instantiate from Enumerable , so it should be instantiate from List
System.Collections.Generic.IEnumerable<Employee> empIEnumerable = new List<Employee>
{   new Employee { ID = 1001, Name="Mahsa"},
    new Employee { ID = 1002, Name = "Hassankashi" },
    new Employee { ID = 1003, Name = "CosmicVerse" },
    new Employee { ID = 1004, Name = "Technical" }
};

this.GridViewIEnumerable.DataSource = empIEnumerable;
this.GridViewIEnumerable.DataBind();

System.Text.StringBuilder str = new System.Text.StringBuilder();

foreach (Employee item in empIEnumerable)
{
    str.Append(" , " + item.ID +"-"+item.Name);
}

this.lblIEnumerable.Text = str.ToString();

IQueryable

Whenever we encounter huge data with so many records, we have to reduce overhead from application. IQueryable prepares high performance in such situations (huge data) by filtering data firstly and then sending filtered data to client.

Image 11

DataAccessEntities ctx = new DataAccessEntities();
        var ctx = new DataAccessEntities();    

Image 12

//Difference between IQueryable and IEnumerable
//You can instantiate IEnumerable from List
IEnumerable<employee> queryIEnumerable = new List<employee>() ;

//Bring  ALL records from server --> to client then filter collection
//To bring all data from server you should omit where clause from linq to sql
queryIEnumerable = from m in ctx.Employees select m;

//If you use where as extension method with IEnumerable then All records will be loaded
queryIEnumerable = queryIEnumerable.Where(x => x.ID == 1).ToList();

//You cannot instantiate IQueryable

IQueryable<employee> queryIQueryable=null;

//Bring just ONE record from server --> to client

queryIQueryable = (from m in ctx.Employees
             where m.ID == 1
             select m);

//Whenever you call IQueryable so ==> It will be executed
this.GridViewIQueryable.DataSource = queryIQueryable.ToList();
this.GridViewIQueryable.DataBind();

SQL Profiler

How to Trace Your Query Generates TSQL & How Many Records Will Be Loaded

Step 1

Start -> MS SQL Server 2008 -> Performance Tools -> SQL Server Profiler

Image 13

Step 2

SQL Server Profiler -> File -> New Trace

Image 14

Step 3

Connect with your user name and password.

Image 15

Step 4

General (Tab) -> Use the Template: Standard

Image 16

Step 5

Event Selection (Tab) -> Event : TSQL -> Select : SQL-BatchCompleted | Select Show all Columns

Press Column Filter -> Database Name: Like: "DataAccess"

Press Run.

Image 17

Step 6

Go To MS SQL Server Management Studio -> Count all of records (records=5)

Image 18

Step 7

IEnumerable Generates
SELECT 
[Extent1].[ID] AS [ID], 
[Extent1].[Name] AS [Name], 
[Extent1].[Age] AS [Age]
FROM [dbo].[Employee] AS [Extent1]

Image 19

IQueryable Generates
SELECT 
[Extent1].[ID] AS [ID], 
[Extent1].[Name] AS [Name], 
[Extent1].[Age] AS [Age]
FROM [dbo].[Employee] AS [Extent1]
WHERE 1 = [Extent1].[ID]

Image 20

ICollection

ICollection inherits from IEnumerable. There is one difference:

You can find IEnumerable[ i ] --> Index Based

You can NOT find ICollection[ i ] --> Not Index Based

Image 21

//IList {indexer and Modify} vs ICollection {randomly and Modify}
//Collection can not be instantiate from ICollection , so it should be instantiate from List
System.Collections.Generic.ICollection<string> strICollection = new List<string>();
strICollection.Add("Mahsa");
strICollection.Add("Hassankashi");

//Countable***
int ICollectionCount=strICollection.Count;

this.ListBoxICollection.DataSource = strICollection;
this.ListBoxICollection.DataBind();
System.Text.StringBuilder str = new System.Text.StringBuilder();
foreach (var item in strICollection)
{
    str.Append(" , " + item);
}
this.lblICollection.Text = str.ToString();

//IList***
System.Collections.Generic.IList<Employee> objIList = new List<Employee>();
objIList = (from m in ctx.Employees
            select m).ToList();

Employee obj = objIList.Where(i => i.Name == "Sara").FirstOrDefault();
int indexofSara= objIList.IndexOf(obj);
int cIList = objIList.Count;

//ICollection***
System.Collections.Generic.ICollection<Employee> objICollection = new List<Employee>();
objICollection = (from m in ctx.Employees
                  select m).ToList();
Employee objIC = objICollection.Where(i => i.Name == "Sara").FirstOrDefault();
//You can not get index of object , if you clear comment from below code appears error
// int indexofSaraICollection = objIC.IndexOf(objIC);
int cICollection = objICollection.Count;

Stack Generic

Push

//Stack is LIFO: Last in First Out
       //Here is for Push Stack in Generic
       //System.Collections.Stack objStackPush = new System.Collections.Stack();
       //Stack<T> can be instantiated from Stack<T>

       System.Collections.Generic.Stack<int> objStackPush = new System.Collections.Generic.Stack<int>();

       objStackPush.Push(1);
       objStackPush.Push(2);

       this.lblPopGeneric.Text = "";
       this.ListBoxStackGeneric.DataSource = objStackPush.ToArray();
       this.ListBoxStackGeneric.DataBind();

Pop

//Stack is LIFO: Last in First Out
//Here is for Pop Stack in Generic
//System.Collections.Stack objStackPop = new System.Collections.Stack();
//Stack<T> can be instantiated from Stack<T>

System.Collections.Generic.Stack<int> objStackPop = new System.Collections.Generic.Stack<int>();

objStackPop.Push(1);
objStackPop.Push(2);

this.lblPop.Text = objStackPop.Pop().ToString();
this.ListBoxStack.DataSource = objStackPop.ToArray();
this.ListBoxStack.DataBind();

Queue Generic

Enqueue

//Queue is FIFO: First in First Out
//Here is for Enqueue Queue in Generic
//System.Collections.Queue objQueue = new System.Collections.Queue();
//Queue<T> can be instantiated from Queue<T>

System.Collections.Generic.Queue<int> objQueue = new System.Collections.Generic.Queue<int>();
objQueue.Enqueue(1);
objQueue.Enqueue(2);

this.lblQueue.Text = "";

this.ListBoxQueue.DataSource = objQueue.ToArray();
this.ListBoxQueue.DataBind();

Dequeue

//Queue is FIFO: First in First Out
//Here is for Dequeue Queue in Generic
//System.Collections.Queue objQueue = new System.Collections.Queue();
//Queue<T> can be instantiated from Queue<T>

System.Collections.Generic.Queue<int> objQueue = new System.Collections.Generic.Queue<int>();

objQueue.Enqueue(1);
objQueue.Enqueue(2);

this.lblQueue.Text = objQueue.Dequeue().ToString();

this.ListBoxQueue.DataSource = objQueue.ToArray();
this.ListBoxQueue.DataBind();

Dictionary and IDictionary

Dictionary is GENERIC while HashTable is not generic: Dictionary<TKey, TValue>. There is a tiny difference between them that if dictionary cannot find specific key will throw an exception while HashTable just returns null.

IDictionary is interface and if you estimate big changes in future, use IDictionary instead of Dictionary.

//Dictionary can instantiate from Dictionary , Dictionary is similar to Hashtable,
//Dictionary is GENERIC but Hashtable is NON GENERIC
//Such Hashtable you can find object by its key
System.Collections.Generic.Dictionary<int,
string=""> objDictionary = new Dictionary<int, string="">();

objDictionary.Add(1001, "Mahsa");
objDictionary.Add(1002, "Hassankashi");
objDictionary.Add(1003, "Cosmicverse");

string str = objDictionary[1002];

this.ListBoxDictionary.DataSource = objDictionary;
this.ListBoxDictionary.DataBind();</int,></int,>

References

Feedback

Feel free to leave any feedback on this article; it is a pleasure to see your comments and vote about this code. If you have any questions, please do not hesitate to ask me here.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mahsa Hassankashi
Doctorandin Technische Universität Berlin
Iran (Islamic Republic of) Iran (Islamic Republic of)
I have been working with different technologies and data more than 10 years.
I`d like to challenge with complex problem, then make it easy for using everyone. This is the best joy.

ICT Master in Norway 2013
Doctorandin at Technische Universität Berlin in Data Scientist ( currently )
-------------------------------------------------------------
Diamond is nothing except the pieces of the coal which have continued their activities finally they have become Diamond.

http://www.repocomp.com/

Comments and Discussions

 
Questionperfect ! Pin
Member 1478154824-Mar-20 2:52
MemberMember 1478154824-Mar-20 2:52 
QuestionVery nice article! Pin
Siavash Mortazavi10-Apr-19 9:54
MemberSiavash Mortazavi10-Apr-19 9:54 
BugIEnumerable is NOT index based Pin
Kirk Wood5-Apr-19 3:18
MemberKirk Wood5-Apr-19 3:18 
GeneralRe: IEnumerable is NOT index based Pin
Mahsa Hassankashi5-Apr-19 4:32
mvaMahsa Hassankashi5-Apr-19 4:32 
GeneralRe: IEnumerable is NOT index based Pin
Kirk Wood9-Apr-19 17:41
MemberKirk Wood9-Apr-19 17:41 
GeneralRe: IEnumerable is NOT index based Pin
Mahsa Hassankashi9-Apr-19 21:20
mvaMahsa Hassankashi9-Apr-19 21:20 
GeneralRe: IEnumerable is NOT index based Pin
Xequence11-Apr-19 2:25
MemberXequence11-Apr-19 2:25 
GeneralRe: IEnumerable is NOT index based Pin
Mel Pama24-Apr-19 14:26
professionalMel Pama24-Apr-19 14:26 
GeneralRe: IEnumerable is NOT index based Pin
Mahsa Hassankashi25-Apr-19 0:27
mvaMahsa Hassankashi25-Apr-19 0:27 
GeneralRe: IEnumerable is NOT index based Pin
mag1314-Sep-19 4:48
Membermag1314-Sep-19 4:48 
GeneralArrays are not actually fixed length Pin
qmartens5-Apr-19 2:31
Memberqmartens5-Apr-19 2:31 
QuestionMajor lacks and minor ones Pin
Daniele Rota Nodari4-Apr-19 8:25
MemberDaniele Rota Nodari4-Apr-19 8:25 
AnswerRe: Major lacks and minor ones Pin
Mahsa Hassankashi4-Apr-19 12:48
mvaMahsa Hassankashi4-Apr-19 12:48 
QuestionNice for junior Pin
Fabrice Avaux4-Apr-19 5:35
MemberFabrice Avaux4-Apr-19 5:35 
AnswerRe: Nice for junior Pin
#realJSOP4-Apr-19 8:19
mva#realJSOP4-Apr-19 8:19 
AnswerRe: Nice for junior Pin
Mahsa Hassankashi4-Apr-19 12:48
mvaMahsa Hassankashi4-Apr-19 12:48 
QuestionYou forgot to include HashSet Pin
#realJSOP4-Apr-19 2:36
mva#realJSOP4-Apr-19 2:36 
AnswerRe: You forgot to include HashSet Pin
Mahsa Hassankashi4-Apr-19 12:37
mvaMahsa Hassankashi4-Apr-19 12:37 
AnswerRe: You forgot to include HashSet Pin
Mahsa Hassankashi4-Apr-19 12:46
mvaMahsa Hassankashi4-Apr-19 12:46 
GeneralRe: You forgot to include HashSet Pin
#realJSOP4-Apr-19 15:19
mva#realJSOP4-Apr-19 15:19 
QuestionVery good and helpful article Pin
rahul kumar nandan3-Apr-19 23:20
Memberrahul kumar nandan3-Apr-19 23:20 
GeneralVery nice article Pin
Nitin J. Jain13-Oct-18 1:23
professionalNitin J. Jain13-Oct-18 1:23 
PraiseNice explanation! Pin
Syed Naqiul Hasan2-Mar-18 7:07
MemberSyed Naqiul Hasan2-Mar-18 7:07 
QuestionGood, you have explanned well- please share the link where i can find all the technical blogs of you ? Pin
Member 1101231325-Oct-17 2:20
MemberMember 1101231325-Oct-17 2:20 
GeneralBest article Pin
Jyoti Kumari9624-Oct-17 8:46
MemberJyoti Kumari9624-Oct-17 8:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.