Index: src/wp-includes/class-wp-state.php
===================================================================
--- src/wp-includes/class-wp-state.php	(revision 0)
+++ src/wp-includes/class-wp-state.php	(working copy)
@@ -0,0 +1,192 @@
+<?php
+/**
+ * State API: WP_State class
+ *
+ * @package WordPress
+ * @subpackage Multisite
+ * @since 4.7.0
+ */
+
+/**
+ * Core class used for handling switched state of the current site and network.
+ *
+ * @since 4.7.0
+ */
+class WP_State {
+	/**
+	 * Stack of the switched sites.
+	 *
+	 * @since 4.7.0
+	 * @access private
+	 * @var array
+	 */
+	private $switched_stack = array();
+
+	/**
+	 * Switches the current site.
+	 *
+	 * This function is useful if you need to pull posts, or other information,
+	 * from other sites. You can switch back afterwards using WP_State::restore_current_site().
+	 *
+	 * If the target site is part of a different network, the network is switched as well.
+	 *
+	 * Things that aren't switched:
+	 *  - autoloaded options. See #14992
+	 *  - plugins. See #14941
+	 *
+	 * @see WP_State::restore_current_site()
+	 *
+	 * @since 4.7.0
+	 * @access public
+	 *
+	 * @global WP_Site $current_blog
+	 *
+	 * @param int $new_site The id of the site you want to switch to. Default: current site
+	 * @return true Always returns true.
+	 */
+	public function switch_to_site( $new_site ) {
+		$old_site = $GLOBALS['current_blog'];
+		$new_site = get_site( $new_site );
+
+		$this->switched_stack[] = $old_site;
+
+		return $this->perform_switch( $new_site, $old_site );
+	}
+
+	/**
+	 * Restores the current site, after calling WP_State::switch_to_site().
+	 *
+	 * If the current site is part of a different network, the network is restored as well.
+	 *
+	 * @see WP_State::switch_to_site()
+	 *
+	 * @since 4.7.0
+	 * @access public
+	 *
+	 * @global WP_Site $current_blog
+	 *
+	 * @return bool True on success, false if we're already on the current site.
+	 */
+	public function restore_current_site() {
+		if ( empty( $this->switched_stack ) ) {
+			return false;
+		}
+
+		$old_site = $GLOBALS['current_blog'];
+		$new_site = array_pop( $this->switched_stack );
+
+		return $this->perform_switch( $new_site, $old_site );
+	}
+
+	/**
+	 * Determines if WP_State::switch_to_site() is in effect
+	 *
+	 * @since 4.7.0
+	 * @access public
+	 *
+	 * @return bool True if switched, false otherwise.
+	 */
+	public function is_switched() {
+		return ! empty( $this->switched_stack );
+	}
+
+	/**
+	 * Switched the current state from one site to another.
+	 *
+	 * @since 4.7.0
+	 * @access private
+	 *
+	 * @global WP_Site    $current_blog
+	 * @global WP_Network $current_site
+	 * @global bool       $switched
+	 *
+	 * @param WP_Site $new_site The site to switch to.
+	 * @param WP_Site $old_site The site to switch from.
+	 * @return true Always returns true.
+	 */
+	private function perform_switch( $new_site, $old_site ) {
+		/*
+		 * If we're switching to the same site that we're on,
+		 * set the right vars, do the associated actions, but skip
+		 * the extra unnecessary work.
+		 */
+		if ( $new_site->id === $old_site->id ) {
+			/**
+			 * Fires when the site is switched.
+			 *
+			 * @since MU
+			 *
+			 * @param int $new_site_id New site ID.
+			 * @param int $old_site_id Old site ID.
+			 */
+			do_action( 'switch_blog', $new_site->id, $new_site->id );
+			$GLOBALS['switched'] = ! empty( $this->switched_stack );
+
+			return true;
+		}
+
+		$GLOBALS['current_blog'] = $new_site;
+
+		if ( $new_site->network_id !== $old_site->network_id ) {
+			$GLOBALS['current_site'] = get_network( $new_site->network_id );
+		}
+
+		$this->initialize_after_switch();
+
+		/* This filter is documented in wp-includes/class-wp-state.php */
+		do_action( 'switch_blog', $new_site->id, $old_site->id );
+		$GLOBALS['switched'] = ! empty( $this->switched_stack );
+
+		return true;
+	}
+
+	/**
+	 * Reinitializes the necessary parts of WordPress after the current site has been switched.
+	 *
+	 * @since 4.7.0
+	 * @access private
+	 *
+	 * @global wpdb            $wpdb
+	 * @global WP_Site         $current_blog
+	 * @global WP_Network      $current_site
+	 * @global int             $blog_id
+	 * @global string          $table_prefix
+	 * @global WP_Object_Cache $wp_object_cache
+	 */
+	private function initialize_after_switch() {
+		global $wpdb, $current_blog, $current_site, $blog_id, $table_prefix;
+
+		$wpdb->set_blog_id( $current_blog->id, $current_site->id );
+		$table_prefix = $wpdb->get_blog_prefix();
+		$blog_id = $current_blog->id;
+
+		if ( function_exists( 'wp_cache_switch_to_blog' ) ) {
+			wp_cache_switch_to_blog( $blog_id );
+		} else {
+			global $wp_object_cache;
+
+			if ( is_object( $wp_object_cache ) && isset( $wp_object_cache->global_groups ) ) {
+				$global_groups = $wp_object_cache->global_groups;
+			} else {
+				$global_groups = false;
+			}
+
+			wp_cache_init();
+
+			if ( function_exists( 'wp_cache_add_global_groups' ) ) {
+				if ( is_array( $global_groups ) ) {
+					wp_cache_add_global_groups( $global_groups );
+				} else {
+					wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'useremail', 'userslugs', 'site-transient', 'site-options', 'site-lookup', 'blog-lookup', 'blog-details', 'rss', 'global-posts', 'blog-id-cache', 'networks', 'sites', 'site-details' ) );
+				}
+				wp_cache_add_non_persistent_groups( array( 'counts', 'plugins' ) );
+			}
+		}
+
+		if ( did_action( 'init' ) ) {
+			wp_roles()->reinit();
+			$current_user = wp_get_current_user();
+			$current_user->for_blog( $blog_id );
+		}
+	}
+}
Index: src/wp-includes/ms-blogs.php
===================================================================
--- src/wp-includes/ms-blogs.php	(revision 38533)
+++ src/wp-includes/ms-blogs.php	(working copy)
@@ -752,84 +752,14 @@
  * @see restore_current_blog()
  * @since MU
  *
- * @global wpdb            $wpdb
- * @global int             $blog_id
- * @global array           $_wp_switched_stack
- * @global bool            $switched
- * @global string          $table_prefix
- * @global WP_Object_Cache $wp_object_cache
+ * @global WP_State $wp_state
  *
- * @param int  $new_blog   The id of the blog you want to switch to. Default: current blog
+ * @param int|WP_Site $new_blog The id or object of the blog you want to switch to. Default: current blog
  * @param bool $deprecated Deprecated argument
  * @return true Always returns True.
  */
 function switch_to_blog( $new_blog, $deprecated = null ) {
-	global $wpdb;
-
-	$blog_id = get_current_blog_id();
-	if ( empty( $new_blog ) ) {
-		$new_blog = $blog_id;
-	}
-
-	$GLOBALS['_wp_switched_stack'][] = $blog_id;
-
-	/*
-	 * If we're switching to the same blog id that we're on,
-	 * set the right vars, do the associated actions, but skip
-	 * the extra unnecessary work
-	 */
-	if ( $new_blog == $blog_id ) {
-		/**
-		 * Fires when the blog is switched.
-		 *
-		 * @since MU
-		 *
-		 * @param int $new_blog New blog ID.
-		 * @param int $new_blog Blog ID.
-		 */
-		do_action( 'switch_blog', $new_blog, $new_blog );
-		$GLOBALS['switched'] = true;
-		return true;
-	}
-
-	$wpdb->set_blog_id( $new_blog );
-	$GLOBALS['table_prefix'] = $wpdb->get_blog_prefix();
-	$prev_blog_id = $blog_id;
-	$GLOBALS['blog_id'] = $new_blog;
-
-	if ( function_exists( 'wp_cache_switch_to_blog' ) ) {
-		wp_cache_switch_to_blog( $new_blog );
-	} else {
-		global $wp_object_cache;
-
-		if ( is_object( $wp_object_cache ) && isset( $wp_object_cache->global_groups ) ) {
-			$global_groups = $wp_object_cache->global_groups;
-		} else {
-			$global_groups = false;
-		}
-		wp_cache_init();
-
-		if ( function_exists( 'wp_cache_add_global_groups' ) ) {
-			if ( is_array( $global_groups ) ) {
-				wp_cache_add_global_groups( $global_groups );
-			} else {
-				wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'useremail', 'userslugs', 'site-transient', 'site-options', 'site-lookup', 'blog-lookup', 'blog-details', 'rss', 'global-posts', 'blog-id-cache', 'networks', 'sites', 'site-details' ) );
-			}
-			wp_cache_add_non_persistent_groups( array( 'counts', 'plugins' ) );
-		}
-	}
-
-	if ( did_action( 'init' ) ) {
-		wp_roles()->reinit();
-		$current_user = wp_get_current_user();
-		$current_user->for_blog( $new_blog );
-	}
-
-	/** This filter is documented in wp-includes/ms-blogs.php */
-	do_action( 'switch_blog', $new_blog, $prev_blog_id );
-	$GLOBALS['switched'] = true;
-
-	return true;
+	return $GLOBALS['wp_state']->switch_to_site( $new_blog );
 }
 
 /**
@@ -838,74 +768,12 @@
  * @see switch_to_blog()
  * @since MU
  *
- * @global wpdb            $wpdb
- * @global array           $_wp_switched_stack
- * @global int             $blog_id
- * @global bool            $switched
- * @global string          $table_prefix
- * @global WP_Object_Cache $wp_object_cache
+ * @global WP_State $wp_state
  *
  * @return bool True on success, false if we're already on the current blog
  */
 function restore_current_blog() {
-	global $wpdb;
-
-	if ( empty( $GLOBALS['_wp_switched_stack'] ) ) {
-		return false;
-	}
-
-	$blog = array_pop( $GLOBALS['_wp_switched_stack'] );
-	$blog_id = get_current_blog_id();
-
-	if ( $blog_id == $blog ) {
-		/** This filter is documented in wp-includes/ms-blogs.php */
-		do_action( 'switch_blog', $blog, $blog );
-		// If we still have items in the switched stack, consider ourselves still 'switched'
-		$GLOBALS['switched'] = ! empty( $GLOBALS['_wp_switched_stack'] );
-		return true;
-	}
-
-	$wpdb->set_blog_id( $blog );
-	$prev_blog_id = $blog_id;
-	$GLOBALS['blog_id'] = $blog;
-	$GLOBALS['table_prefix'] = $wpdb->get_blog_prefix();
-
-	if ( function_exists( 'wp_cache_switch_to_blog' ) ) {
-		wp_cache_switch_to_blog( $blog );
-	} else {
-		global $wp_object_cache;
-
-		if ( is_object( $wp_object_cache ) && isset( $wp_object_cache->global_groups ) ) {
-			$global_groups = $wp_object_cache->global_groups;
-		} else {
-			$global_groups = false;
-		}
-
-		wp_cache_init();
-
-		if ( function_exists( 'wp_cache_add_global_groups' ) ) {
-			if ( is_array( $global_groups ) ) {
-				wp_cache_add_global_groups( $global_groups );
-			} else {
-				wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'useremail', 'userslugs', 'site-transient', 'site-options', 'site-lookup', 'blog-lookup', 'blog-details', 'rss', 'global-posts', 'blog-id-cache', 'networks', 'sites', 'site-details' ) );
-			}
-			wp_cache_add_non_persistent_groups( array( 'counts', 'plugins' ) );
-		}
-	}
-
-	if ( did_action( 'init' ) ) {
-		wp_roles()->reinit();
-		$current_user = wp_get_current_user();
-		$current_user->for_blog( $blog );
-	}
-
-	/** This filter is documented in wp-includes/ms-blogs.php */
-	do_action( 'switch_blog', $blog, $prev_blog_id );
-
-	// If we still have items in the switched stack, consider ourselves still 'switched'
-	$GLOBALS['switched'] = ! empty( $GLOBALS['_wp_switched_stack'] );
-
-	return true;
+	return $GLOBALS['wp_state']->restore_current_site();
 }
 
 /**
@@ -913,12 +781,12 @@
  *
  * @since 3.5.0
  *
- * @global array $_wp_switched_stack
+ * @global WP_State $wp_state
  *
  * @return bool True if switched, false otherwise.
  */
 function ms_is_switched() {
-	return ! empty( $GLOBALS['_wp_switched_stack'] );
+	return $GLOBALS['wp_state']->is_switched();
 }
 
 /**
Index: src/wp-includes/ms-settings.php
===================================================================
--- src/wp-includes/ms-settings.php	(revision 38533)
+++ src/wp-includes/ms-settings.php	(working copy)
@@ -28,6 +28,9 @@
 /** WP_Site class */
 require_once( ABSPATH . WPINC . '/class-wp-site.php' );
 
+/* WP_State class */
+require_once( ABSPATH . WPINC . '/class-wp-state.php' );
+
 /** Multisite loader */
 require_once( ABSPATH . WPINC . '/ms-load.php' );
 
@@ -88,8 +91,8 @@
 $wpdb->set_prefix( $table_prefix, false ); // $table_prefix can be set in sunrise.php
 $wpdb->set_blog_id( $current_blog->blog_id, $current_blog->site_id );
 $table_prefix = $wpdb->get_blog_prefix();
-$_wp_switched_stack = array();
 $switched = false;
+$wp_state = new WP_State();
 
 // need to init cache again after blog_id is set
 wp_start_object_cache();
